This commit is contained in:
2025-11-30 14:49:16 +08:00
parent 021c593241
commit de46c4b073
1406 changed files with 526774 additions and 1221 deletions

View File

@@ -0,0 +1,301 @@
from maya import cmds
from maya.api import OpenMaya as om2
#import logging
'''
Create in ADV 6.040, select the corresponding IKFK controller, and simply run the script to switch
by kangddan
'''
#logger = logging.getLogger(__name__)
def setAttrs(nodes, attrs, value):
for n in nodes:
for ids, i in enumerate(attrs):
try:
if isinstance(value[ids], (list, tuple)):
cmds.setAttr('{}.{}'.format(n, i), *value[ids])
else:
cmds.setAttr('{}.{}'.format(n, i), value[ids])
except:
pass
class Path:
@classmethod
def path(cls, fullPathName):
return fullPathName.split("|")[-1]
@classmethod
def name(cls, fullPathName):
return cls.path(fullPathName).split(':')[-1]
@classmethod
def namespace(cls, fullPathName):
return cls.path(fullPathName).rpartition(':')[0]
class MOpenMaya:
@staticmethod
def toMObject(nodeName):
sl = om2.MSelectionList(); sl.add(nodeName)
return sl.getDependNode(0)
@classmethod
def getDagNode(cls, mobj) :
if not mobj.hasFn(om2.MFn.kDagNode): return
return om2.MDagPath.getAPathTo(mobj)
@classmethod
def getGlobatMatrix(cls, dagNode):
if not isinstance(dagNode, om2.MDagPath): return
return dagNode.inclusiveMatrix()
@classmethod
def getGlobalPos(cls, dagNode):
if not dagNode: return
tm = om2.MTransformationMatrix(cls.getGlobatMatrix(dagNode))
return tm.translation(om2.MSpace.kTransform)
@classmethod
def ls(cls):
sl = om2.MGlobal.getActiveSelectionList()
return [cls.getDagNode(sl.getDependNode(o)) for o in range(sl.length())]
class IkFk:
ARM_IK_CTRLS = {'ik1':'IKArm', 'ik2':'PoleArm'} # 0
ARM_FK_CTRLS = {'fk1':'FKShoulder', 'fk2':'FKElbow', 'fk3':'FKWrist'} # 1
ARM_FKTOIK_LOCS = {'loc1':'IKX2Shoulder', 'loc2':'IKX2Elbow', 'loc3':'IKX2Wrist'} # 2
ARM_IKTOFK_LOCS = {'loc1':'AlignIKToWrist'} # 3
ARM_IKFK = {'switch':'FKIKArm'}
LEG_IK_CTRLS = {'ik1':'IKLeg', 'ik2':'PoleLeg', 'ik3':'RollHeel', 'ik4':'RollToesEnd', 'ik5':'RollToes', 'ik6':'IKToes'}
LEG_FK_CTRLS = {'fk1':'FKHip', 'fk2':'FKKnee', 'fk3':'FKAnkle', 'fk4':'FKToes'}
LEG_FKTOIK_LOCS = {'loc1':'IKX2Hip', 'loc2':'IKX2Knee', 'loc3':'IKX2Ankle', 'loc4':'IKX2Toes', 'loc5':'IKX2ToesEnd'}
LEG_IKTOFK_LOCS = {'loc1':'AlignIKToAnkle', 'loc2':'AlignIKToToes'}
leg_IKFK = {'switch':'FKIKLeg'}
# --------------------------------------------------------------
ARM_BASE_JNTS = {'base1':'Shoulder', 'base2':'Elbow', 'base3':'Wrist', 'root':'RootSystem'}
LEG_BASE_JNTS = {'base1':'Hip', 'base2':'Knee', 'base3':'Ankle', 'root':'RootSystem'}
# --------------------------------------------------------------
@staticmethod
def getLRDict(_dict, namespace=''):
ldictList = [{k:'{}{}_L'.format(namespace, v) for k, v in d.items()} for d in _dict[:-2]]
rdictList = [{k:'{}{}_R'.format(namespace, v) for k, v in d.items()} for d in _dict[:-2]]
basedictList = [{k:'{}{}'.format(namespace, v) for k, v in d.items()} for d in _dict[-2:]]
return ldictList, rdictList, basedictList
@staticmethod
def getLongName(sName, _dict):
for k, v in _dict.items():
longNames = cmds.ls(v, long=True)
for ln in longNames:
if sName == ln[0:len(sName)]:
_dict[k] = ln
# if ln[0].startswith(sName):
# _dict[k] = ln
break
# --------------------------------------------------------------
def newDict(self, fullPathName):
dictList = [IkFk.ARM_IK_CTRLS, IkFk.ARM_FK_CTRLS, IkFk.ARM_FKTOIK_LOCS, IkFk.ARM_IKTOFK_LOCS, IkFk.ARM_IKFK,
IkFk.LEG_IK_CTRLS, IkFk.LEG_FK_CTRLS, IkFk.LEG_FKTOIK_LOCS, IkFk.LEG_IKTOFK_LOCS, IkFk.leg_IKFK,
IkFk.ARM_BASE_JNTS, IkFk.LEG_BASE_JNTS]
namespace = Path.namespace(fullPathName)
if namespace:
self.isRef = True
return self.getLRDict(dictList, namespace+':')
else:
self.isRef = False
sName = '|' + cmds.ls('*{}'.format(fullPathName), long=True)[0].split('|')[1]
ldictList, rdictList, basedictList = self.getLRDict(dictList)
for dL, dR in zip(ldictList, rdictList):
self.getLongName(sName, dL)
self.getLongName(sName, dR)
for bS in basedictList: self.getLongName(sName, bS) #
return ldictList, rdictList, basedictList
@staticmethod
def setPolePose(jntList, pole):
jntPos = [MOpenMaya.getGlobalPos(MOpenMaya.getDagNode(MOpenMaya.toMObject(j))) for j in jntList]
off = ((jntPos[1] - jntPos[0]).length() + (jntPos[2] - jntPos[1]).length())
midPos = ((jntPos[1] - jntPos[0]).normal() + ((jntPos[2] - jntPos[1]).normal() * -1)) * 0.5
polePos = (midPos.normal() * off) + jntPos[1]
cmds.xform(pole, t=polePos, ws=True)
@staticmethod
def setIkStretch(jntList, locList):
jntPos = [MOpenMaya.getGlobalPos(MOpenMaya.getDagNode(MOpenMaya.toMObject(j))) for j in jntList]
locPos = [MOpenMaya.getGlobalPos(MOpenMaya.getDagNode(MOpenMaya.toMObject(l))) for l in locList]
upper = (jntPos[1] - jntPos[0]).length()
mid = (jntPos[2] - jntPos[1]).length()
locUpper = (locPos[1] - locPos[0]).length()
locMid = (locPos[2] - locPos[1]).length()
upperError = abs(upper / locUpper)
midErrorr = abs(mid / locMid)
return upperError, midErrorr
def getNewDict(self, fullPathName):
ldictList, rdictList, basedictList = self.newDict(fullPathName)
ALD = ldictList[0:5]; LLD = ldictList[5:]
ARD = rdictList[0:5]; LRD = rdictList[5:]
AB = basedictList[0]; LB = basedictList[1] # base joint ref
return ALD, ARD, LLD, LRD, AB, LB
@staticmethod
def _updateDict(_dict):
return [value for d in [_dict[0], _dict[1], _dict[-1]] for value in d.values()]
def __init__(self, autoK=True):
self.curTime , self.prevTime = cmds.currentTime(q=True), cmds.currentTime(q=True)-1
self.autoK = autoK
self.isRef = False
self.ikfkSwitch()
def autoKey(self, nodes=[], num=None):
if not self.autoK:
return
for i in nodes:
try:
if cmds.keyframe(i, q=True, kc=True):
cmds.setKeyframe(i, i=True, t=num)
else:
cmds.setKeyframe(i, i=False, t=num)
except:
continue
def ikfkSwitch(self):
sl = MOpenMaya.ls()
for i in sl:
if i is None:
om2.MGlobal.displayWarning('Please select a DAG node')
continue
ALD, ARD, LLD, LRD, AB, LB = self.getNewDict(i.fullPathName())
path = Path.path(i.fullPathName()) if self.isRef else i.fullPathName()
if path in self._updateDict(ALD): # L arm
self.armIKFk(ALD, AB)
elif path in self._updateDict(ARD): # R arm
self.armIKFk(ARD, AB)
elif path in self._updateDict(LLD): # L leg
self.legIkFk(LLD, LB)
elif path in self._updateDict(LRD): # R leg
self.legIkFk(LRD, LB)
def armIKFk(self, _dict, armBase):
switch = _dict[-1]['switch']
ikCtrl, poleCtrl = _dict[0]['ik1'], _dict[0]['ik2']
fkCtrl1, fkCtrl2, fkCtrl3 = _dict[1]['fk1'], _dict[1]['fk2'], _dict[1]['fk3']
fti1, fti2, fti3 = _dict[2]['loc1'], _dict[2]['loc2'], _dict[2]['loc3']
itf = _dict[3]['loc1']
baseJ1, baseJ2, baseJ3, root = armBase['base1'], armBase['base2'], armBase['base3'], armBase['root']
# -------------------------------------------
scaleValue = cmds.getAttr('{}.sx'.format(root))
# -------------------------------------------
value = cmds.getAttr(switch + '.FKIKBlend')
self.autoKey([switch, ikCtrl, poleCtrl, fkCtrl1, fkCtrl2, fkCtrl3], self.prevTime)
# fk to ik
if value == 0.0:
upperV, midV = self.setIkStretch([fkCtrl1, fkCtrl2, fkCtrl3], [baseJ1, baseJ2, baseJ3])
cmds.setAttr('{}.{}'.format(ikCtrl, 'Lenght1'), upperV / scaleValue)
cmds.setAttr('{}.{}'.format(ikCtrl, 'Lenght2'), midV/ scaleValue)
cmds.setAttr('{}.{}'.format(ikCtrl, 'stretchy'), 0)
cmds.setAttr('{}.{}'.format(poleCtrl, 'lock'), 0)
cmds.matchTransform(ikCtrl, itf)
self.setPolePose([fkCtrl1, fkCtrl2, fkCtrl3], poleCtrl)
cmds.setAttr(switch + '.FKIKBlend', 10.0)
# ik to fk
elif value == 10.0:
for fk, loc in zip([fkCtrl1, fkCtrl2, fkCtrl3], [fti1, fti2, fti3]):
cmds.matchTransform(fk, loc)
cmds.setAttr(switch + '.FKIKBlend', 0.0)
self.autoKey([switch, ikCtrl, poleCtrl, fkCtrl1, fkCtrl2, fkCtrl3], self.curTime)
def legIkFk(self, _dict, legBase):
switch = _dict[-1]['switch']
ikCtrl, poleCtrl, ik3, ik4, ik5, ik6 = _dict[0]['ik1'], _dict[0]['ik2'], _dict[0]['ik3'], _dict[0]['ik4'], _dict[0]['ik5'], _dict[0]['ik6']
fkCtrl1, fkCtrl2, fkCtrl3, fkCtrl4 = _dict[1]['fk1'], _dict[1]['fk2'], _dict[1]['fk3'], _dict[1]['fk4']
fti1, fti2, fti3, fti4, fti5 = _dict[2]['loc1'], _dict[2]['loc2'], _dict[2]['loc3'], _dict[2]['loc4'], _dict[2]['loc5']
itf1, itf2 = _dict[3]['loc1'], _dict[3]['loc2']
baseJ1, baseJ2, baseJ3, root = legBase['base1'], legBase['base2'], legBase['base3'], legBase['root']
# -------------------------------------------
scaleValue = cmds.getAttr('{}.sx'.format(root))
# -------------------------------------------
value = cmds.getAttr(switch + '.FKIKBlend')
self.autoKey([switch, ikCtrl, poleCtrl, ik3, ik4, ik5, ik6, fkCtrl1, fkCtrl2, fkCtrl3, fkCtrl4], self.prevTime)
# fk to ik
if value == 0.0:
upperV, midV = self.setIkStretch([fkCtrl1, fkCtrl2, fkCtrl3], [baseJ1, baseJ2, baseJ3])
cmds.setAttr('{}.{}'.format(ikCtrl, 'Lenght1'), upperV / scaleValue)
cmds.setAttr('{}.{}'.format(ikCtrl, 'Lenght2'), midV / scaleValue)
cmds.setAttr('{}.{}'.format(ikCtrl, 'stretchy'), 0)
cmds.setAttr('{}.{}'.format(poleCtrl, 'lock'), 0)
setAttrs([ikCtrl], ['swivel', 'roll', 'rock', 'antiPop'], [0, 0, 0, 0])
setAttrs([ik3, ik4, ik5], ['t', 'r', 's'], [[0, 0, 0], [0, 0, 0], [1, 1, 1]])
cmds.matchTransform(ikCtrl, itf1)
cmds.matchTransform(ik6, itf2) #
self.setPolePose([fkCtrl1, fkCtrl2, fkCtrl3], poleCtrl)
cmds.setAttr(switch + '.FKIKBlend', 10.0)
# ik to fk
elif value == 10.0:
for fk, loc in zip([fkCtrl1, fkCtrl2, fkCtrl3, fkCtrl4], [fti1, fti2, fti3, fti4]):
cmds.matchTransform(fk, loc)
cmds.setAttr(switch + '.FKIKBlend', 0.0)
self.autoKey([switch, ikCtrl, poleCtrl, ik3, ik4, ik5, ik6, fkCtrl1, fkCtrl2, fkCtrl3, fkCtrl4], self.curTime)
if __name__ == '__main__':
IkFk(False)
'''
Revision History
Revision 1: 2023-08-11 : First publish
Revision 2: 2023-08-14 : Fix a series of errors in the IK controller
Revision 3: 2023-08-18 : Code Redundancy Optimization
Revision 4: 2023-08-19 : Adding auto keyframe
Revision 5: 2023-08-22 : Update IK Pole Position Algorithm
Revision 6: 2023-09-15 : Optimize the transition from FK to IK
'''

View File

@@ -1,706 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
IKFK Seamless Switching Tool V1.0
IKFK无缝切换工具
使用scriptJob实现实时切换
"""
import maya.cmds as cmds
import maya.mel as mel
class IKFKSwitcher(object):
"""IKFK无缝切换工具主类"""
def __init__(self):
self.window_name = "ikfk_switcher_window"
self.window_title = "IKFK Seamless Switching Tool V1.0"
# 存储控制器和关节
self.joints = {
'shoulder': None,
'elbow': None,
'wrist': None
}
self.fk_ctrls = {
'shoulder': None,
'elbow': None,
'wrist': None
}
self.ik_ctrls = {
'wrist': None,
'pole': None
}
self.switch_ctrl = None
self.switch_attr = None
# scriptJob ID列表
self.script_jobs = []
# UI控件引用
self.ui_elements = {}
def create_ui(self):
"""创建UI界面"""
# 如果窗口存在则删除
if cmds.window(self.window_name, exists=True):
cmds.deleteUI(self.window_name)
# 创建窗口
self.window_name = cmds.window(
self.window_name,
title=self.window_title,
widthHeight=(280, 650),
sizeable=True
)
# 主布局
main_layout = cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
# 标题栏
cmds.rowLayout(numberOfColumns=3, adjustableColumn=2, columnAttach=[(1, 'left', 5)])
cmds.text(label="IKFK Seamless Switching Tool V1.0", font="boldLabelFont")
cmds.button(label="?", width=25, height=25, command=self.show_help)
cmds.button(label="X", width=25, height=25,
backgroundColor=[0.8, 0.3, 0.3],
command=lambda x: cmds.deleteUI(self.window_name))
cmds.setParent('..')
cmds.separator(height=10, style='in')
# Edit菜单
cmds.frameLayout(label="Edit", collapsable=True, collapse=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
cmds.button(label="<<< ADV Build >>>", height=25,
command=self.adv_build,
backgroundColor=[0.4, 0.4, 0.4])
cmds.button(label="<<< Empty >>>", height=25,
command=self.empty_fields,
backgroundColor=[0.4, 0.4, 0.4])
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=10, style='in')
# Load Joint部分
cmds.frameLayout(label="Load Joint", collapsable=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
self.ui_elements['joint_shoulder_field'] = self._create_load_field("Joint Shoulder", self.load_joint_shoulder)
self.ui_elements['joint_elbow_field'] = self._create_load_field("Joint Elbow", self.load_joint_elbow)
self.ui_elements['joint_wrist_field'] = self._create_load_field("Joint Wrist", self.load_joint_wrist)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5)
# Load FK Ctrl部分
cmds.frameLayout(label="Load FK Ctrl", collapsable=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
self.ui_elements['fk_shoulder_field'] = self._create_load_field("FK Shoulder", self.load_fk_shoulder)
self.ui_elements['fk_elbow_field'] = self._create_load_field("FK Elbow", self.load_fk_elbow)
self.ui_elements['fk_wrist_field'] = self._create_load_field("FK Wrist", self.load_fk_wrist)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5)
# Load IK Ctrl部分
cmds.frameLayout(label="Load IK Ctrl", collapsable=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
self.ui_elements['ik_wrist_field'] = self._create_load_field("IK Wrist", self.load_ik_wrist)
self.ui_elements['ik_pole_field'] = self._create_load_field("IK Pole", self.load_ik_pole)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5)
# Load Switch Ctrl部分
cmds.frameLayout(label="Load Switch Ctrl", collapsable=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
self.ui_elements['switch_ctrl_field'] = self._create_load_field("Switch Ctrl", self.load_switch_ctrl)
self.ui_elements['switch_attr_field'] = self._create_load_field("Switch Attr", self.load_switch_attr)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=10, style='in')
# 构建按钮
cmds.button(label=">>> Build Seamless Switching >>>",
height=35,
backgroundColor=[0.4, 0.7, 0.4],
command=self.build_seamless_switching)
cmds.separator(height=5)
# 显示窗口
cmds.showWindow(self.window_name)
def _create_load_field(self, label, command):
"""创建加载字段的辅助方法"""
cmds.rowLayout(numberOfColumns=2, adjustableColumn=1, columnAttach=[(2, 'right', 5)])
text_field = cmds.textField(editable=False, backgroundColor=[0.2, 0.2, 0.2])
cmds.button(label="<<< {} ".format(label), width=120, command=command)
cmds.setParent('..')
return text_field
def load_joint_shoulder(self, *args):
"""加载肩部关节"""
sel = cmds.ls(selection=True)
if sel:
self.joints['shoulder'] = sel[0]
cmds.textField(self.ui_elements['joint_shoulder_field'], edit=True, text=sel[0])
print("Loaded Joint Shoulder: {}".format(sel[0]))
else:
cmds.warning("Please select Joint Shoulder")
def load_joint_elbow(self, *args):
"""加载肘部关节"""
sel = cmds.ls(selection=True)
if sel:
self.joints['elbow'] = sel[0]
cmds.textField(self.ui_elements['joint_elbow_field'], edit=True, text=sel[0])
print("Loaded Joint Elbow: {}".format(sel[0]))
else:
cmds.warning("Please select Joint Elbow")
def load_joint_wrist(self, *args):
"""加载腕部关节"""
sel = cmds.ls(selection=True)
if sel:
self.joints['wrist'] = sel[0]
cmds.textField(self.ui_elements['joint_wrist_field'], edit=True, text=sel[0])
print("Loaded Joint Wrist: {}".format(sel[0]))
else:
cmds.warning("Please select Joint Wrist")
def load_fk_shoulder(self, *args):
"""加载FK肩部控制器"""
sel = cmds.ls(selection=True)
if sel:
self.fk_ctrls['shoulder'] = sel[0]
cmds.textField(self.ui_elements['fk_shoulder_field'], edit=True, text=sel[0])
print("Loaded FK Shoulder: {}".format(sel[0]))
else:
cmds.warning("Please select FK Shoulder control")
def load_fk_elbow(self, *args):
"""加载FK肘部控制器"""
sel = cmds.ls(selection=True)
if sel:
self.fk_ctrls['elbow'] = sel[0]
cmds.textField(self.ui_elements['fk_elbow_field'], edit=True, text=sel[0])
print("Loaded FK Elbow: {}".format(sel[0]))
else:
cmds.warning("Please select FK Elbow control")
def load_fk_wrist(self, *args):
"""加载FK腕部控制器"""
sel = cmds.ls(selection=True)
if sel:
self.fk_ctrls['wrist'] = sel[0]
cmds.textField(self.ui_elements['fk_wrist_field'], edit=True, text=sel[0])
print("Loaded FK Wrist: {}".format(sel[0]))
else:
cmds.warning("Please select FK Wrist control")
def load_ik_wrist(self, *args):
"""加载IK腕部控制器"""
sel = cmds.ls(selection=True)
if sel:
self.ik_ctrls['wrist'] = sel[0]
cmds.textField(self.ui_elements['ik_wrist_field'], edit=True, text=sel[0])
print("Loaded IK Wrist: {}".format(sel[0]))
else:
cmds.warning("Please select IK Wrist control")
def load_ik_pole(self, *args):
"""加载IK极向量控制器"""
sel = cmds.ls(selection=True)
if sel:
self.ik_ctrls['pole'] = sel[0]
cmds.textField(self.ui_elements['ik_pole_field'], edit=True, text=sel[0])
print("Loaded IK Pole: {}".format(sel[0]))
else:
cmds.warning("Please select IK Pole control")
def load_switch_ctrl(self, *args):
"""加载切换控制器"""
sel = cmds.ls(selection=True)
if sel:
self.switch_ctrl = sel[0]
cmds.textField(self.ui_elements['switch_ctrl_field'], edit=True, text=sel[0])
print("Loaded Switch Control: {}".format(sel[0]))
else:
cmds.warning("Please select Switch control")
def load_switch_attr(self, *args):
"""加载切换属性"""
sel = cmds.ls(selection=True)
if sel and self.switch_ctrl:
# 获取选中的通道
channels = cmds.channelBox('mainChannelBox', query=True, selectedMainAttributes=True)
if channels:
self.switch_attr = channels[0]
cmds.textField(self.ui_elements['switch_attr_field'], edit=True, text=self.switch_attr)
print("Loaded Switch Attribute: {}".format(self.switch_attr))
else:
cmds.warning("Please select an attribute in the Channel Box")
else:
cmds.warning("Please load Switch Control first and select an attribute")
def build_seamless_switching(self, *args):
"""构建无缝切换系统"""
# 验证所有必需的控制器和关节是否已加载
if not self._validate_inputs():
return
# 清除现有的scriptJob
self.clear_script_jobs()
# 创建scriptJob来监听切换属性的变化
if self.switch_ctrl and self.switch_attr:
attr_full_name = "{}.{}".format(self.switch_ctrl, self.switch_attr)
# 创建属性变化监听
job_id = cmds.scriptJob(
attributeChange=[attr_full_name, self.on_switch_changed],
killWithScene=True,
protected=True
)
self.script_jobs.append(job_id)
print("Created scriptJob (ID: {}) for attribute: {}".format(job_id, attr_full_name))
cmds.inViewMessage(
amg='<hl>Seamless switching built successfully!</hl>',
pos='midCenter',
fade=True,
fadeStayTime=2000,
fadeOutTime=500
)
else:
cmds.warning("Switch control or attribute not loaded")
def _validate_inputs(self):
"""验证所有输入是否有效"""
# 检查关节
for key, joint in self.joints.items():
if not joint or not cmds.objExists(joint):
cmds.warning("Joint {} not loaded or doesn't exist".format(key))
return False
# 检查FK控制器
for key, ctrl in self.fk_ctrls.items():
if not ctrl or not cmds.objExists(ctrl):
cmds.warning("FK control {} not loaded or doesn't exist".format(key))
return False
# 检查IK控制器
for key, ctrl in self.ik_ctrls.items():
if not ctrl or not cmds.objExists(ctrl):
cmds.warning("IK control {} not loaded or doesn't exist".format(key))
return False
# 检查切换控制器
if not self.switch_ctrl or not cmds.objExists(self.switch_ctrl):
cmds.warning("Switch control not loaded or doesn't exist")
return False
if not self.switch_attr:
cmds.warning("Switch attribute not loaded")
return False
return True
def on_switch_changed(self):
"""当切换属性改变时调用"""
if not self.switch_ctrl or not self.switch_attr:
return
# 获取切换值 (假设0=FK, 1=IK)
switch_value = cmds.getAttr("{}.{}".format(self.switch_ctrl, self.switch_attr))
# 根据切换值进行匹配
if switch_value == 0:
# 切换到FK - 匹配FK到IK
self.match_fk_to_ik()
elif switch_value == 1:
# 切换到IK - 匹配IK到FK
self.match_ik_to_fk()
def match_fk_to_ik(self):
"""匹配FK控制器到IK位置"""
try:
# 匹配FK肩部到关节
if self.fk_ctrls['shoulder'] and self.joints['shoulder']:
cmds.matchTransform(self.fk_ctrls['shoulder'], self.joints['shoulder'],
position=False, rotation=True)
# 匹配FK肘部到关节
if self.fk_ctrls['elbow'] and self.joints['elbow']:
cmds.matchTransform(self.fk_ctrls['elbow'], self.joints['elbow'],
position=False, rotation=True)
# 匹配FK腕部到关节
if self.fk_ctrls['wrist'] and self.joints['wrist']:
cmds.matchTransform(self.fk_ctrls['wrist'], self.joints['wrist'],
position=False, rotation=True)
print("Matched FK to IK")
except Exception as e:
cmds.warning("Error matching FK to IK: {}".format(str(e)))
def match_ik_to_fk(self):
"""匹配IK控制器到FK位置"""
try:
# 匹配IK腕部到关节
if self.ik_ctrls['wrist'] and self.joints['wrist']:
cmds.matchTransform(self.ik_ctrls['wrist'], self.joints['wrist'],
position=True, rotation=True)
# 计算并设置极向量位置
if self.ik_ctrls['pole'] and all(self.joints.values()):
pole_pos = self._calculate_pole_vector_position()
if pole_pos:
cmds.xform(self.ik_ctrls['pole'], worldSpace=True,
translation=pole_pos)
print("Matched IK to FK")
except Exception as e:
cmds.warning("Error matching IK to FK: {}".format(str(e)))
def _calculate_pole_vector_position(self):
"""计算极向量位置"""
try:
# 获取三个关节的世界坐标
start_pos = cmds.xform(self.joints['shoulder'], query=True,
worldSpace=True, translation=True)
mid_pos = cmds.xform(self.joints['elbow'], query=True,
worldSpace=True, translation=True)
end_pos = cmds.xform(self.joints['wrist'], query=True,
worldSpace=True, translation=True)
# 使用向量计算极向量位置
import maya.OpenMaya as om
start_vec = om.MVector(start_pos[0], start_pos[1], start_pos[2])
mid_vec = om.MVector(mid_pos[0], mid_pos[1], mid_pos[2])
end_vec = om.MVector(end_pos[0], end_pos[1], end_pos[2])
# 计算从起点到终点的向量
start_to_end = end_vec - start_vec
start_to_mid = mid_vec - start_vec
# 投影
scale = (start_to_end * start_to_mid) / (start_to_end * start_to_end)
proj = start_to_end * scale + start_vec
# 计算极向量方向
pole_dir = mid_vec - proj
pole_dir.normalize()
# 计算链长度
length = (mid_vec - start_vec).length() + (end_vec - mid_vec).length()
# 极向量位置
pole_pos = mid_vec + pole_dir * length
return [pole_pos.x, pole_pos.y, pole_pos.z]
except Exception as e:
cmds.warning("Error calculating pole vector position: {}".format(str(e)))
return None
def clear_script_jobs(self):
"""清除所有scriptJob"""
for job_id in self.script_jobs:
if cmds.scriptJob(exists=job_id):
cmds.scriptJob(kill=job_id, force=True)
print("Killed scriptJob ID: {}".format(job_id))
self.script_jobs = []
def adv_build(self, *args):
"""高级构建功能 - 专门适配AdvanceSkeleton骨骼"""
# 获取当前选择
selection = cmds.ls(selection=True)
if not selection:
cmds.warning("Please select an AdvanceSkeleton control first")
return
# 尝试自动检测AdvanceSkeleton的IKFK_Seamless属性
ctrl = selection[0]
# 检查是否有IKFK_Seamless属性
if cmds.attributeQuery('IKFK_Seamless', node=ctrl, exists=True):
# 自动设置切换控制器和属性
self.switch_ctrl = ctrl
self.switch_attr = 'IKFK_Seamless'
cmds.textField(self.ui_elements['switch_ctrl_field'], edit=True, text=ctrl)
cmds.textField(self.ui_elements['switch_attr_field'], edit=True, text='IKFK_Seamless')
# 尝试自动查找相关的FK和IK控制器
self._auto_detect_adv_skeleton_controls(ctrl)
# 提示用户
result = cmds.confirmDialog(
title='AdvanceSkeleton Detected',
message='Detected AdvanceSkeleton control with IKFK_Seamless attribute.\n\nSwitch Control: {}\nSwitch Attribute: IKFK_Seamless\n\nPlease verify and load remaining controls manually if needed.'.format(ctrl),
button=['Build Now', 'Cancel'],
defaultButton='Build Now',
cancelButton='Cancel',
dismissString='Cancel'
)
if result == 'Build Now':
self.build_seamless_switching()
else:
# 没有找到IKFK_Seamless属性
cmds.confirmDialog(
title='Not AdvanceSkeleton',
message='Selected control does not have IKFK_Seamless attribute.\n\nThis ADV Build feature is designed for AdvanceSkeleton rigs.\n\nPlease select an AdvanceSkeleton FK or IK control.',
button=['OK'],
defaultButton='OK'
)
def _auto_detect_adv_skeleton_controls(self, ctrl):
"""自动检测AdvanceSkeleton控制器"""
# 获取命名空间和控制器名称
namespace = ''
ctrl_name = ctrl
if ':' in ctrl:
parts = ctrl.split(':')
namespace = ':'.join(parts[:-1]) + ':'
ctrl_name = parts[-1]
print("Auto-detecting AdvanceSkeleton controls for: {}".format(ctrl_name))
# 确定是左侧还是右侧
side = ''
if ctrl_name.endswith('_L'):
side = '_L'
elif ctrl_name.endswith('_R'):
side = '_R'
# 检测手臂控制器
if 'Arm' in ctrl_name:
self._load_adv_arm_controls(namespace, side)
# 检测腿部控制器
elif 'Leg' in ctrl_name:
self._load_adv_leg_controls(namespace, side)
else:
print("Unknown limb type, please load controls manually")
def _load_adv_arm_controls(self, namespace, side):
"""加载AdvanceSkeleton手臂控制器"""
print("Loading AdvanceSkeleton Arm controls for side: {}".format(side))
# AdvanceSkeleton手臂命名规范
# 关节
shoulder_jnt = "{}FKShoulder{}".format(namespace, side)
elbow_jnt = "{}FKElbow{}".format(namespace, side)
wrist_jnt = "{}FKWrist{}".format(namespace, side)
# FK控制器
fk_shoulder = "{}FKShoulder{}".format(namespace, side)
fk_elbow = "{}FKElbow{}".format(namespace, side)
fk_wrist = "{}FKWrist{}".format(namespace, side)
# IK控制器
ik_wrist = "{}IKArm{}".format(namespace, side)
ik_pole = "{}PoleArm{}".format(namespace, side)
# 尝试加载关节
self._try_load_control(shoulder_jnt, 'joint', 'shoulder')
self._try_load_control(elbow_jnt, 'joint', 'elbow')
self._try_load_control(wrist_jnt, 'joint', 'wrist')
# 尝试加载FK控制器
self._try_load_control(fk_shoulder, 'fk', 'shoulder')
self._try_load_control(fk_elbow, 'fk', 'elbow')
self._try_load_control(fk_wrist, 'fk', 'wrist')
# 尝试加载IK控制器
self._try_load_control(ik_wrist, 'ik', 'wrist')
self._try_load_control(ik_pole, 'ik', 'pole')
def _load_adv_leg_controls(self, namespace, side):
"""加载AdvanceSkeleton腿部控制器"""
print("Loading AdvanceSkeleton Leg controls for side: {}".format(side))
# AdvanceSkeleton腿部命名规范
# 关节
hip_jnt = "{}FKHip{}".format(namespace, side)
knee_jnt = "{}FKKnee{}".format(namespace, side)
ankle_jnt = "{}FKAnkle{}".format(namespace, side)
# FK控制器
fk_hip = "{}FKHip{}".format(namespace, side)
fk_knee = "{}FKKnee{}".format(namespace, side)
fk_ankle = "{}FKAnkle{}".format(namespace, side)
# IK控制器
ik_ankle = "{}IKLeg{}".format(namespace, side)
ik_pole = "{}PoleLeg{}".format(namespace, side)
# 尝试加载关节
self._try_load_control(hip_jnt, 'joint', 'shoulder') # 使用shoulder位置存储hip
self._try_load_control(knee_jnt, 'joint', 'elbow') # 使用elbow位置存储knee
self._try_load_control(ankle_jnt, 'joint', 'wrist') # 使用wrist位置存储ankle
# 尝试加载FK控制器
self._try_load_control(fk_hip, 'fk', 'shoulder')
self._try_load_control(fk_knee, 'fk', 'elbow')
self._try_load_control(fk_ankle, 'fk', 'wrist')
# 尝试加载IK控制器
self._try_load_control(ik_ankle, 'ik', 'wrist')
self._try_load_control(ik_pole, 'ik', 'pole')
def _try_load_control(self, ctrl_name, ctrl_type, position):
"""尝试加载控制器到对应位置"""
if cmds.objExists(ctrl_name):
if ctrl_type == 'joint':
self.joints[position] = ctrl_name
field_name = 'joint_{}_field'.format(position)
if field_name in self.ui_elements:
cmds.textField(self.ui_elements[field_name], edit=True, text=ctrl_name)
print(" Loaded Joint {}: {}".format(position, ctrl_name))
elif ctrl_type == 'fk':
self.fk_ctrls[position] = ctrl_name
field_name = 'fk_{}_field'.format(position)
if field_name in self.ui_elements:
cmds.textField(self.ui_elements[field_name], edit=True, text=ctrl_name)
print(" Loaded FK {}: {}".format(position, ctrl_name))
elif ctrl_type == 'ik':
self.ik_ctrls[position] = ctrl_name
field_name = 'ik_{}_field'.format(position)
if field_name in self.ui_elements:
cmds.textField(self.ui_elements[field_name], edit=True, text=ctrl_name)
print(" Loaded IK {}: {}".format(position, ctrl_name))
else:
print(" Warning: Control not found: {}".format(ctrl_name))
def empty_fields(self, *args):
"""清空所有字段"""
# 清空关节字段
for key in self.joints.keys():
self.joints[key] = None
# 清空FK控制器字段
for key in self.fk_ctrls.keys():
self.fk_ctrls[key] = None
# 清空IK控制器字段
for key in self.ik_ctrls.keys():
self.ik_ctrls[key] = None
# 清空切换控制器
self.switch_ctrl = None
self.switch_attr = None
# 清空UI文本框
for field_name, field_widget in self.ui_elements.items():
if cmds.textField(field_widget, exists=True):
cmds.textField(field_widget, edit=True, text='')
# 清除scriptJob
self.clear_script_jobs()
print("All fields cleared")
cmds.inViewMessage(
amg='<hl>All fields cleared!</hl>',
pos='midCenter',
fade=True,
fadeStayTime=1000,
fadeOutTime=500
)
def show_help(self, *args):
"""显示帮助信息"""
help_text = """
IKFK Seamless Switching Tool - Help
使用步骤:
1. 加载关节 (Load Joint)
- 依次选择并加载肩部、肘部、腕部关节
2. 加载FK控制器 (Load FK Ctrl)
- 依次选择并加载FK肩部、肘部、腕部控制器
3. 加载IK控制器 (Load IK Ctrl)
- 选择并加载IK腕部控制器和极向量控制器
4. 加载切换控制器 (Load Switch Ctrl)
- 选择切换控制器
- 在通道盒中选择切换属性后点击加载
5. 构建无缝切换
- 点击 "Build Seamless Switching" 按钮
- 系统将创建scriptJob监听切换属性
- 当切换属性改变时自动匹配IK/FK位置
注意:
- scriptJob会在场景关闭时自动清除
- 切换属性值: 0=FK, 1=IK
"""
result = cmds.confirmDialog(
title='Help',
message=help_text,
button=['OK'],
defaultButton='OK'
)
def close_window(self):
"""关闭窗口并清理"""
self.clear_script_jobs()
if cmds.window(self.window_name, exists=True):
cmds.deleteUI(self.window_name)
# 全局实例
_ikfk_switcher_instance = None
def show():
"""显示IKFK切换工具窗口"""
global _ikfk_switcher_instance
# 如果实例存在,先清理
if _ikfk_switcher_instance:
_ikfk_switcher_instance.close_window()
# 创建新实例
_ikfk_switcher_instance = IKFKSwitcher()
_ikfk_switcher_instance.create_ui()
return _ikfk_switcher_instance
def start():
"""启动函数(兼容性别名)"""
return show()
if __name__ == "__main__":
show()

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Site : CGNICO Games
# @Author : Jeffrey Tsai
# @Site : Virtuos Games
# @Author : Cai Jianbo
"""
Maya Batch Extrusion Shell Mesh Tool

View File

@@ -0,0 +1,210 @@
"""
GS CurveTools License:
This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi)
and can not be copied or distributed without his written permission.
GS CurveTools v1.3.8 Personal
Copyright 2024, George Sladkovsky (Yehor Sladkovskyi)
All Rights Reserved
UI font is Roboto that is licensed under the Apache 2.0 License:
http://www.apache.org/licenses/LICENSE-2.0
Autodesk Maya is a property of Autodesk, Inc:
https://www.autodesk.com/
Social Media and Contact Links:
Discord Server: https://discord.gg/f4DH6HQ
Online Store: https://sladkovsky3d.artstation.com/store
Online Documentation: https://gs-curvetools.readthedocs.io/
Twitch Channel: https://www.twitch.tv/videonomad
YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky
ArtStation Portfolio: https://www.artstation.com/sladkovsky3d
Contact Email: george.sladkovsky@gmail.com
"""
https://www.artstation.com/marketplace-product-eula
Marketplace Product & Services Agreement
End User Agreement
This Marketplace End User Agreement applies to all downloadable products and professional services (e.g. mentorships, personal training, portfolio reviews) sold via the ArtStation Marketplace, unless a custom agreement or license is provided by the seller.
The EUA is an agreement between the buyer and the seller providing the goods or services.
PLEASE READ THIS DOCUMENT CAREFULLY. IT SIGNIFICANTLY ALTERS YOUR LEGAL RIGHTS AND REMEDIES.
BY CLICKING “I AGREE” OR DOWNLOADING OR USING THE DIGITAL PRODUCT OR RECEIVING THE PROFESSIONAL SERVICES TO WHICH THIS AGREEMENT RELATES YOU ACCEPT ALL OF THIS AGREEMENTS TERMS, INCLUDING THE DISCLAIMERS OF WARRANTIES AND LIMITATIONS ON DAMAGES, USE AND TRANSFERABILITY. IF YOU DO NOT ACCEPT THIS AGREEMENTS TERMS, DO NOT DOWNLOAD, INSTALL OR USE THE DIGITAL PRODUCT OR RECEIVE OR USE THE PROFESSIONAL SERVICES.
This end-user agreement (“Agreement”) is a legally binding agreement between you, the licensee and customer (“you” or “your”), and the provider (“we” or “us” or “our”) of the digital products (“Products”) or instructional, training, mentorship or other professional service packages (“Professional Services”) that you purchase through the ArtStation Marketplace, regarding your rights and obligations regarding those Products and Professional Services.
1. Your Status
In this Agreement, “you” means the person or entity acquiring rights in the Products or purchasing Professional Services. That may be a natural person, or a corporate or business entity or organization.
(a) If you are a natural person then you must be, and you confirm that you are, at least 13 years old. If you are between 13 years and the age of majority in your jurisdiction of residence, you confirm that your parent or legal guardian has reviewed and agrees to this Agreement and is happy for you to access and use the Product or receive the Professional Services.
(b) If you are a corporate entity then: (i) the rights granted under this Agreement are granted to that entity; (ii) you represent and warrant that the individual completing and accepting this Agreement is an authorized your representative and has the authority to legally bind that you to the Agreement; and (iii) to the extent that one or more of your employees are granted any rights in the Product or to receive Professional Services under this Agreement, you will ensure that your employees comply with this Agreement and you will be responsible and liable for any breach of this Agreement by any employee.
2. ArtStation
ArtStation is a division of Epic Games, Inc., You acknowledge and agree that Epic is a third-party beneficiary of this Agreement and therefore will be entitled to directly enforce and rely upon any provision in this Agreement that confers a benefit on, or rights in favour of, Epic. In addition, you authorize Epic to act as your authorized representative to file a lawsuit or other formal action against a licensor in a court or with any other governmental authority if Epic knows or suspects that a licensor breached any representations or warranties under this Agreement. The foregoing authorization is nonexclusive, and Epic shall be under no obligation to pursue any claim. Epic will not initiate any such action on your behalf without first consulting with and obtaining your approval.
Products
The following sections 3 through 9 apply to any Products you acquire from us through the ArtStation Marketplace:
3. Product Licence
Subject to this Agreements terms and conditions, we hereby grant you a limited, non-exclusive, worldwide, non-transferable right and licence to (which will be perpetual unless the licence terminates as set out in this Agreement): (a) download the Product; and (b) copy and use the Product. We reserve all rights not expressly granted to you under this Agreement.
4. Licence Scope and Restrictions
(a) Tutorials
You are purchasing ONE licence to create ONE copy of the Product for use by you only (or, if you are a corporate entity, for use by a single authorized employee).
If this Product is bundled with a stock digital asset then you receive a limited personal use licence regarding that stock digital asset, and you may use that stock digital asset for your personal use only. You will not use that stock digital asset in any commercial manner unless you purchase a separate commercial licence.
(b) Installable Tools
You may purchase one or more licences for the Product. A single licence allows you to install the Product on a single computer at a time for use by a single authorized user. If you are a corporate entity and the authorized employee completing the transaction on your behalf purchases multiple licences, you may choose to store the Product on a single server or shared hard drive for use by a single authorized employee at a time for each licence purchased.
Provided that you comply with the restrictions on users set out above, you may use the Product on an unlimited number of projects.
(c) Stock Assets
Subject to the restrictions set out in this Agreement, you may copy, use, modify, adapt, translate, distribute, publicly display, transmit, broadcast, and create derivative works from the Product in works you create (“Works”), which may include things like films, videos, multi-media projects, computer games, models, images, publications, broadcasts, documents, and presentations.
If you are a corporate entity, you may make the Product available for use by your employees in accordance with this Agreement (for example, by storing the Product on a network server).
You may only share the Product with external people or entities where:
- You are collaborating with the external parties in the creation of your Work and you need to share the Product for that purpose, provided that any external party that receives the Product may only use it in your Work and must secure and limit access to the Product for that purpose;
- You are working as a contractor for a client in the creation of a Work and need to share the Product with your client, or any external parties working with your client, provided that your client and any such external parties may use the Product only for your clients Work, and all parties secure and limit access to the Product for that purpose.
For any other use of the Product by any other party, that party must purchase a licence to the Product.
In addition to any other restrictions in this Agreement, you will not:
- publish, sell, license, offer or make available for sale or licensing, or otherwise distribute the Product except as part of a Work or through a form of sharing that is authorized in this Agreement; or
- publish, distribute or make available the Product through any online clearinghouse platform.
FURTHER SPECIFIC TERMS
In addition to the restrictions set out above, the following terms and conditions apply to the following forms of commercial licences for the Product:
Standard Commercial Licence
If you have purchased a Standard Commercial licence then you may exercise your rights under that licence:
- for personal use on an unlimited number of personal projects that are not used or distributed in any commercial manner; and
- respect to one commercial Work, with up to a maximum of, as applicable, 2,000 sales of the Work or 20,000 monthly views of the Work.
Extended Commercial Licence
If you have purchased an Extended Commercial licence then you may exercise your rights under that licence:
- for personal use on an unlimited number of personal projects that are not used or distributed in any commercial manner; and
- with respect to any number of commercial Works, with no limit on sales or views.
5. Additional Restrictions
Except as expressly permitted under this Agreement, you will not:
(a) make any copy of the Product except for archival or backup purposes;
(b) circumvent or disable any access control technology, security device, procedure, protocol, or technological protection mechanism that may be included or established in or as part of the Product;
(c) hack, reverse engineer, decompile, disassemble, modify or create derivative works of the Product or any part of the Product;
(d) publish, sell distribute or otherwise make the Product available to others to use, download or copy;
(e) transfer or sub-license the Product or any rights under this Agreement to any third party, whether voluntarily or by operation of law;
(f) use the Product for any purpose that may be defamatory, threatening, abusive, harmful or invasive of anyones privacy, or that may otherwise violate any law or give rise to civil or other liability;
(g) misrepresent yourself as the creator or owner of the Property;
(h) remove or modify any proprietary notice, symbol or label in or on the Product;
(i) directly or indirectly assist, facilitate or encourage any third party to carry on any activity prohibited by this Agreement.
6. Proprietary Rights
The Product is protected by copyright laws and international copyright treaties, as well as other intellectual property laws and treaties. You are licensing the Product and the right to access, install and use the Product in accordance with this Agreement, not buying the Product. As between you and us, we own all right, title and interest in and to the Product, and you are not acquiring any ownership of or rights in the Product except the limited rights granted under this Agreement.
7. No Epic Support
You acknowledge and agree that you are licensing the Product from us (the Provider), not from Epic, and that Epic has no obligation to support the Product.
8. Interruptions and Errors
Your use of the Product might be interrupted and might not be free of errors.
9. Updates
We have no obligation to update the Product.
Professional Services
The following sections 10 and 11 apply to any Professional Services you purchase from us through the ArtStation Marketplace:
10. Provision of Professional Services
We will provide the Professional Services directly to you and, subject to this Agreement, will assume all responsibility for all aspects of the Professional Services. We represent and warrant that we have the right to offer and provide the Professional Services and that we have appropriate qualifications and experience to provide the Professional Services.
11. Epic is not Involved
You acknowledge and agree that:
(a) Epic is only a provider of the online ArtStation Marketplace where you purchased the Professional Services, and does not provide or exercise any control or oversight over us or the Professional Services, and is not responsible for us or the Professional Services or any shortcomings in them, including any damages, losses or legal issues caused by us or the Professional Services;
(b) this Agreement (and any dispute under it) is an agreement between us and you only, and not with Epic, and Epic is not a party to this Agreement;
(c) we are not Epics employee, agent or subcontractor;
(d) Epic does not have any obligation to attempt to resolve any dispute between us and you; and
(e) we will provide the Professional Services directly to you, and we (and not Epic) are solely responsible for the Professional Services, and Epic has no obligation or liability to you with respect to the Professional Services.
Both Products and Services
The following sections 12 through 25 apply to all Products or Services you purchase from us through the ArtStation Marketplace:
12. Disclaimer
ANY PRODUCTS OR PROFESSIONAL SERVICES ARE PROVIDED ON AN “AS IS” AND “AS AVAILABLE” BASIS, WITHOUT ANY REPRESENTATIONS, WARRANTIES OR CONDITIONS OF ANY KIND.
TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW WE DISCLAIM, AND YOU WAIVE (WITH REGARD TO US AND ALSO TO EPIC, ITS AFFILIATES, AND ITS AND THEIR LICENSORS AND SERVICE PROVIDERS (COLLECTIVELY, THE “EPIC PARTIES”), ALL TERMS, CONDITIONS, GUARANTEES, REPRESENTATIONS AND WARRANTIES (EXPRESS, IMPLIED, STATUTORY AND OTHERWISE), IN RESPECT OF THE PRODUCTS AND PROFESSIONAL SERVICES, INCLUDING THOSE OF MERCHANTABILITY, NON-INFRINGEMENT, TITLE, QUALITY AND FITNESS FOR A PARTICULAR PURPOSE.
NEITHER WE NOR ANY OF THE EPIC PARTIES REPRESENT OR WARRANT THAT: (A) ANY PRODUCT OR PROFESSIONAL SERVICE IS ACCURATE, COMPLETE, RELIABLE, CURRENT OR ERROR-FREE; (B) ANY PRODUCT OR PROFESSIONAL SERVICE WILL MEET YOUR REQUIREMENTS OR EXPECTATIONS; (C) ANY PRODUCT OR PROFESSIONAL SERVICES IS FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS; OR (D) ANY DEFECTS IN ANY PRODUCT OR PROFESSIONAL SERVICE WILL BE CORRECTED.
13. Exclusion and Limitation of Liability
(a) YOU DOWNLOAD, INSTALL AND OTHERWISE USE ALL PRODUCTS, AND RECEIVE AND USE ALL PROFESSIONAL SERVICES, AT YOUR OWN RISK. YOU AGREE TO, AND HEREBY DO:
(i) WAIVE ANY CLAIMS THAT YOU MAY HAVE AGAINST US OR THE EPIC PARTIES OR OUR RESPECTIVE DIRECTORS, OFFICERS, EMPLOYEES, AGENTS, REPRESENTATIVES, LICENSORS, SUCCESSORS AND ASSIGNS (COLLECTIVELY THE “RELEASEES”) ARISING FROM OR RELATING TO ANY PRODUCTS OR PROFESSIONAL SERVICES, AND
(ii) RELEASE THE RELEASEES FROM ANY LIABILITY FOR ANY LOSS, DAMAGE, EXPENSE OR INJURY ARISING FROM OR RELATING TO YOUR USE OF ANY PRODUCT OR PROFESSIONAL SERVICE, WHETHER ARISING IN TORT (INCLUDING NEGLIGENCE), CONTRACT OR OTHERWISE, EVEN IF THE RELEASEES ARE EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH LOSS, INJURY OR DAMAGE AND EVEN IF THAT LOSS, INJURY OR DAMAGE IS FORESEEABLE.
(b) NEITHER WE NOR THE EPIC PARTIES WILL BE LIABLE FOR ANY LOSSES, DAMAGES, CLAIMS OR EXPENSES THAT CONSTITUTE: (I) LOSS OF INTEREST, PROFIT, BUSINESS, CUSTOMERS OR REVENUE; (II) BUSINESS INTERRUPTIONS; (III) COST OF REPLACEMENT PRODUCTS OR SERVICES; OR (IV) LOSS OF OR DAMAGE TO REPUTATION OR GOODWILL.
(c) NEITHER WE NOR THE EPIC PARTIES WILL BE LIABLE FOR ANY LOSSES, DAMAGES, CLAIMS OR EXPENSES THAT CONSTITUTE INCIDENTAL, CONSEQUENTIAL, SPECIAL, PUNITIVE, EXEMPLARY, MULTIPLE OR INDIRECT DAMAGES, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, DAMAGES, CLAIMS OR EXPENSES.
(d) MAXIMUM LIABILITY: IF, DESPITE THE LIMITATIONS SET OUT ABOVE, WE OR ANY EPIC PARTY BECOME LIABLE TO YOU IN RESPECT OF ANY PRODUCT OR PROFESSIONAL SERVICE OR OTHERWISE UNDER THIS AGREEMENT, THE ENTIRE CUMULATIVE LIABILITY OF US AND THE EPIC PARTIES, AND YOUR EXCLUSIVE AND CUMULATIVE REMEDY, FOR ANY DAMAGES (REGARDLESS OF THE CAUSE OR FORM OR ACTION), WILL BE LIMITED TO CAD$10.
14. Indemnity
As a condition of your use of any Product or any Professional Services, you agree to hold harmless and indemnify the Releasees from any liability for any loss or damage to any third party resulting from your access to, installation or use of the Product or your receipt and use of the Professional Services.
15. Term and Termination
This Agreement is effective until terminated. Your rights under this Agreement will terminate automatically without notice if: (a) you breach any terms of this Agreement; or (b) you do not complete payment for the Product or Professional Services, or any payment you make is refunded, reversed or cancelled for any reason. Upon this Agreements termination, you will cease all use of the Product and destroy all copies, full or partial, of the Product in your possession. Sections 11 through 25 will survive the termination of this Agreement.
16. Compliance with Laws
You will comply with all applicable laws when using any Product or Professional Services (including intellectual property and export control laws).
17. Entire Agreement
This Agreement supersedes all prior agreements of the parties regarding the Product or Professional Services, and constitutes the whole agreement with respect to the Product or Professional Services.
18. Disputes
If you have any concerns about the Product or Professional Services, please contact us through our ArtStation Marketplace account and we will work with you to try to resolve the issue. You acknowledge and agree that any such dispute is between you and us, and that Epic will not be involved in the dispute and has no obligation to try to resolve the dispute.
19. Persons Bound
This Agreement will enure to the benefit of and be binding upon the parties and their heirs, executors, administrators, legal representatives, lawful successors and permitted assigns.
20. Assignment
We may assign this Agreement without notice to you. You may not assign this Agreement or any of your rights under it without our prior written consent, which we will not withhold unreasonably.
21. Waiver
No waiver, delay, or failure to act by us regarding any particular default or omission will prejudice or impair any of our rights or remedies regarding that or any subsequent default or omission that are not expressly waived in writing.
22. Applicable Law and Jurisdiction
You agree that this Agreement will be deemed to have been made and executed in the State of North Carolina, U.S.A., and any dispute will be resolved in accordance with the laws of North Carolina, excluding that body of law related to choice of laws, and of the United States of America. Any action or proceeding brought to enforce the terms of this Agreement or to adjudicate any dispute must be brought in the Superior Court of Wake County, State of North Carolina or the United States District Court for the Eastern District of North Carolina. You agree to the exclusive jurisdiction and venue of these courts. You waive any claim of inconvenient forum and any right to a jury trial. The Convention on Contracts for the International Sale of Goods will not apply. Any law or regulation which provides that the language of a contract shall be construed against the drafter will not apply to this Agreement.
23. Legal Effect
This Agreement describes certain legal rights. You may have other rights under the laws of your country. This Agreement does not change your rights under the laws of your country if the laws of your country do not permit it to do so.
24. Interpretation
In this Agreement, "we", "us", and "our" refer to the licensor of the Product alone and never refer to the combination of you and that licensor (that combination is referred to as "the parties"), or the combination of you or the licensor with Epic.
25. Artificial Intelligence
For purposes of this Agreement, “Generative AI Programs” means artificial intelligence, machine learning, deep learning, neural networks, or similar technologies designed to automate the generation of or aid in the creation of new content, including but not limited to audio, visual, or text-based content.
We (the licensor of the Product) represent and warrant that where the Product was created using Generative AI Programs, we have applied the “CreatedWithAI” tag. Under this Agreement, a Product is considered to be created using Generative AI Programs where a material portion of a Product is generated with Generative AI Programs, whether characters, backgrounds, or other material elements. A Product is not considered to be created using Generative AI Programs merely for use of features that solely operate on a Product (e.g., AI-based upscaling or content-aware fill).

View File

@@ -0,0 +1,35 @@
GS CurveTools installation
1. Copy gs_curvetools folder to {Path to Documents}\Documents\Maya\{Maya Version}\scripts\
Example of the final folder structure:
Documents\Maya\2022\scripts\gs_curvetools\fonts
Documents\Maya\2022\scripts\gs_curvetools\icons
Documents\Maya\2022\scripts\gs_curvetools\utils
Documents\Maya\2022\scripts\gs_curvetools\__init__.py
Documents\Maya\2022\scripts\gs_curvetools\core.py
Documents\Maya\2022\scripts\gs_curvetools\init.py
Documents\Maya\2022\scripts\gs_curvetools\LICENSE.txt
Documents\Maya\2022\scripts\gs_curvetools\main.py
Documents\Maya\2022\scripts\gs_curvetools\README.txt
2. Run Maya
3. Copy and paste this line to "Python" command box and press "Enter":
import gs_curvetools.init as ct_init;from imp import reload;reload(ct_init);ct_init.Init();
IMPORTANT: There should be no spaces or tabs before this command!
4. Look for GS tab on your Shelf
5. Click CT UI button to run the menu. Click again to hide the menu.
NOTES:
>> To reset to factory defaults click CT with "refresh" arrow button.
>> To stop all scripts and close the menu press CT DEL button.
>> You can use middle-mouse button drag to move the buttons to any tab.
>> All the hotkeys are available in Hotkey Editor > Custom Scripts > GS > GS_CurveTools.
>> Always repeat initialization steps when updating the plug-in to a new version.
>> You can always repeat initialization steps if you lost control buttons or shelf.

View File

@@ -0,0 +1,951 @@
<!--
Tooltips are added in format:
# Widget
Tooltip
-->
<!-- main.py -->
<!-- Options Menu -->
# importCurves
Imports Curves that were Exported using the Export Button.
NOTE: Only import files that were exported using Export Curves button.
WARNING: This operation is NOT undoable!
# exportCurves
Exports selected curves into a .curves (or .ma) file. Can then be Imported using Import Curves.
# changeScaleFactor
Opens a window that controls the Scale Factor and Precision Scale.
***
Scale factor determines the initial size of the created curve and adjusts other parameters.
Scale factor is stored in a global preset, in the scene and on each curve.
Priority of scale factors Curve>Scene>Global.
Setting the appropriate scale factor before starting a project can help to have a good initial Width and size of the cards.
***
Precision Scale controls the world scale of the curve without changing the curve appearance, CV positions and other parameters.
Lowering the Precision scale (from 1.0 to 0.05 for example) helps to fix the geometry deformation on very small curves.
The default value of 0.05 should be sufficient for most cases. Values lower than 0.01 can affect performance.
***
Normalize Selected and Normalize Selected to Default will change the Scale Factor and Precision Scale of the selected curves, without changing their appearance or CV positions.
Normalize Selected to Default will reset the slider values to default (0.5 and 0.05) and Normalize Selected will use the current slider values.
# normalizeSelectedButton
Normalize selected curves to the chosen slider values.
Normalization will not change the CV positions or geometry appearance, but the selected objects will have new Scale Factor and Precision Scale applied.
# normalizeSelectedToDefaultButton
Normalize selected curves to the default slider values (0.5, 0.05).
Resets the slider values to the default.
Normalization will not change the CV positions or geometry appearance, but the selected objects will have new Scale Factor and Precision Scale applied.
# saveScaleFactorAndPrecisionScaleButton
Saves the selected slider values to the global storage and to the current scene.
New scenes will automatically have these Scale Factor and Precision Scale values.
NOTE: This will not apply the new values to already existing objects. See Normalize buttons.
# saveScaleFactorAndPrecisionScaleButtonAndClose
Saves the selected slider values to the global storage and to the current scene.
New scenes will automatically have these Scale Factor and Precision Scale values.
Closes the window after save.
NOTE: This will not apply the new values to already existing objects. See Normalize buttons.
# closePrecisionScaleWindow
Close this window without save
# globalCurveThickness
Opens a window that controls the thickness of the curves in the scene as well as the global curve thickness preset across the scenes.
# setAOSettings
Manually sets the recommended AO settings for older Maya versions.
These AO settings are needed to use the "See Through" or "Toggle Always on Top" functions.
Are applied automatically by those functions.
# setTransparencySettings
Sets recommended transparency settings for Maya viewport.
***
Simple Transparency is fast but very inaccurate render mode. Only suitable for simple, one layer hair.
Object Sorting Transparency has average performance impact and quality. Can have issues on complex multi-layered grooms.
Depth Transparency - these are the optimal settings for the highest quality of the hair cards preview. Can have performance impact on slower systems.
# convertToWarpCard
Converts selection to Warp Cards.
Compatible attributes are retained.
# convertToWarpTube
Converts selection to Warp Tubes.
Compatible attributes are retained.
# convertToExtrudeCard
Converts selection to Extrude Cards.
Compatible attributes are retained.
# convertToExtrudeTube
Converts selection to Extrude Tubes.
Compatible attributes are retained.
# duplicateUnparentCurves
Duplicates selected NURBS curves and unparents them (parents them to the world).
Original curves are not deleted.
Can be used to easily extract and export curves from GS CurveTools objects.
# syncCurveColor
Toggles the syncing of the curve color to the layer color.
# colorizedRegroup
Toggles the colorization of the regrouped layers when Regroup by Layer function is used.
# colorOnlyDiffuse
Colorize only the diffuse component of the card material.
Alpha will stay the same.
# checkerPattern
Toggles the use of the checker pattern when the Color mode is enabled.
# ignoreLastLayer
Toggles the filtering (All, Curve, Geo) and Extraction (Extract All) of the last layer. If enabled, the last layer is ignored.
NOTE: Last layer is typically used for templates, so it is ignored by default.
# ignoreTemplateCollections
Ignores the filtering (All, Curve, Geo) and Extraction (Extract All) of all the collections that have "template" in their name. Case insensitive.
# groupTemplateCollections
Collections that have "template" in their name (case insensitive) will be grouped together into "CT_Templates" group by Regroup By Layer function.
# syncOutlinerLayerVis
Syncs the outliner and layer visibility. If enabled, hiding the layer will also hide the curve groups in the outliner.
# keepCurveAttributes
If enabled, the attributes that are stored in the curve will be restored if the curve that was duplicated (on its own, without the geo) is used to create other cards or tubes.
If disabled, the attributes will be ignored and reset.
Example:
1. Create a card and change its twist value.
2. Ctrl+D and Shift+P the curve (not the curve group).
3. Click Curve Card and the twist value will be restored on a newly created card.
# boundCurvesFollowParent
Will ensure that moving a parent curve in a Bound Object (Bound Group) will also move all the child curves along with it to a new layer. Recommended to keep this enabled.
# massBindOption
Will bind selected hair clump (or geometry) to all selected "empty" curves.
# bindDuplicatesCurves
Will automatically duplicate the curves before binding them to the curve, leaving old curves behind with no edits.
# bindFlipUVs
Enabling this option will flip the UVs of the original cards before binding them to the curve.
It will also automatically flip the bound geo and rotate it 90 deg.
This option will also flip the UVs back when using Unbind for better workflow.
Disabling this option will result in an old Bind/Unbind behaviour.
# unpackDeleteOriginalObject
Unpack (Shift + Click on Unbind) will delete the original Bind object.
# unpackCVMatch
Unpack (Shift + Click on Unbind) will match the CVs of the original Bind object.
# addBetweenBlendAttributes
Enables blending of the attributes when using Add Cards/Tubes or Fill functions.
# fillCreateCurvesOnly
When enabled Fill function will only create curves (not the geo).
# convertInstances
Will automatically convert instanced curves to normal curves before any other function is applied.
# replacingCurveLayerSelection
Will disable additive selection for the layers. When holding Ctrl and clicking on a new layer, old layer will be deselected automatically.
# useAutoRefineOnNewCurves
Automatically enables auto-refine on the new curves.
# flipUVsAfterMirror
Enabling this option will flip the UVs horizontally after mirroring the cards to achieve exact mirroring.
# enableTooltips
Will toggle the visibility of these tooltips you are reading right now.
# showLayerCollectionsMenu
Shows layer collections menu widget.
# importIntoANewCollection
If enabled, all the imported curves will be placed into a new "Imported Curves" layer collection.
If disabled, all the imported curves will be placed into the "Main" layer collection
# layerNumbersOnly
Layers will use only numbers if enabled.
# convertToNewLayerSystem
Converts all the layers in the scene to a new layer system that is hidden from the Channel Box Display Layers window.
Layers can still be accessed from Window->Relationship Editors->Display Layer window.
# updateLayers
Utility function that will manually update all layers. Used for if layers are correct for some reason.
# resetToDefaults
Resets every option and the GS CurveTools to the default "factory" state.
# maya2020UVFix
This function will fix any broken UVs when trying to open old scenes in Maya 2020 or 2022 or when opening scenes in 2020 and 2022 when using Maya Binary file type. This will have no effect on older versions of Maya (<2020). This bug is native to Maya and thus cant be fixed in GS CurveTools plug-in.
# mayaFixBrokenGraphs
This function will attempt to fix all the broken graphs in the scene.
Use if one of the graphs (Width, Twist or Profile) is in a broken state.
# convertBezierToNurbs
Converts the selected Bezier curves to NURBS curves.
Bezier curves are not supported by the GS CurveTools.
# maya2020TwistAttribute
This function will fix any broken cards created in Maya 2020.4 before v1.2.2 update.
# maya2020UnbindFix
This function will fix any cards that are not unbinding properly and were created before v1.2.3 update in Maya 2020.4.
# deleteAllAnimationKeys
This function will delete all the animation keys on all the curves present in the scene.
This can fix deformation issues when using duplicate or other GS CurveTools functions.
Some functions (like duplicate) will delete the keys automatically, however the keys can still cause issues.
<!-- Main Buttons -->
# warpSwitch
Advanced cards and tubes suitable for longer hair.
Additional options and controls.
Slower than Extrude (viewport performance).
Can have issues on very small scales.
# extrudeSwitch
Simple cards and tubes suitable for shorter hair and brows.
Has limited controls.
Much faster than Warp (viewport performance).
Better for small scales.
# newCard
Creates a new Card in the middle of the world. Used at the beginning of the project to create templates.
# newTube
Creates a new Tube in the middle of the world. Used at the beginning of the project to create templates.
# curveCard
Converts selected Maya curve to CurveTools Card.
# curveTube
Converts selected Maya curve to CurveTools Tube.
# gsBind
Binds selection to a single curve. Creates a Bind Group. Selection options:
1. Single "empty" curve (default Maya curve) and single combined geometry.
2. Single "empty" curve (default Maya curve) and any number of Curve Cards and Curve Tubes.
***
Shift + Click will duplicate the original curves/geo before binding it to the empty curve.
Same option is available in the Options menu (Duplicate Curves Before Bind).
# gsUnbind
Normal Click:
UnBinds geometry or Cards/Tubes from selected Bound object.
Geometry and Cards/Tubes will be placed at the origin.
***
Shift + Click:
Will UNPACK the selected Bind object in-place.
Unpack will attempt to create an in-place approximation of the cards and tubes that Bind object consists of.
Basically it will extract the geometry and create cards (or tubes) based on that geometry.
The original Bind object will be deleted in the process. Optionally, you can keep it (toggle in the options menu).
This operation is not procedural, so you will not be able to return back to the Bind object after (unlike regular UnBind).
WARNING: Unpack is not a 1 to 1 conversion. It will try its best to approximate the shape, but complex twists and bends will not be captured.
# addCards
Creates Cards in-between selected Cards based on the Add slider value.
Bound objects are NOT supported.
NOTE: Selection order defines the direction of added Cards if more than 2 initial Cards are selected.
# addTubes
Creates Tubes in-between selected Tubes based on the Add slider value.
Bound objects are NOT supported.
NOTE: Selection order defines the direction of added Tubes if more than 2 initial Tubes are selected.
# gsFill
Creates Cards/Tubes or Bound Groups in between selected Cards/Tubes or Bound Groups based on the Add slider value.
NOTE 1: Selection order defines the direction of added curves if more than 2 initial curves are selected.
NOTE 2: The type of Card or Tube or Bound Group is defined by the previous curve in the selection chain.
NOTE 3: Options -> Fill Creates Only Curves option will make the Fill function create only NURBS curves, but not the geo.
# gsSubdivide
Subdivides selected curve into multiple curves based on the Add slider value
Shift + Click subdivides selected curve but does not delete the original curve
# gsEdgeToCurve
Converts selected geometry edges to curves.
Multiple unconnected edge groups can be selected at the same time.
***
Key Combinations:
Shift + Click will create a curve without the curvature (first degree curve or simply a line)
# gsGeoToCurve
Opens the Geo-to-Curve UI
Geo to Curve algorithm will attempt to generate GS CurveTools cards and tubes from selected geometry.
Selected geometry should be either one-sided cards or regular tubes without caps (or both).
Multi-selection compatible, but selected geometries should be separate objects and not one combined object.
# layerCollectionsComboBox
Layer collections drop-down menu.
Allows to separate the project into different layer collections, up to 80 layers in each collection.
Has additional functionality in marking menu (Hold RMB):
***
Marking menu:
1. Clear - will delete all the cards from the current layer. Undoable operation.
2. Merge Up, Merge Down - merge all the cards from the current layer to the layer above or below it.
3. Copy, Paste - will copy all the cards from the current layer and paste them to the layer that user selects.
4. Move Up, Move Down - will rearrange the current layer collections by moving the currently selected collection up or down in the list.
5. Rename - will rename the current layer collection
# layerCollectionsPlus
Add additional layer collection after the current one.
# layerCollectionsMinus
Remove current layer collection. All the cards from the removed collection will be merged one layer UP.
# gsAllFilter
Layer filter. Controls the visibility of all objects in all layers:
Normal click will show all curves and geometry in all layers.
Shift + Click will hide all curves and geometry in all layers
Ctrl + Click will show all the curves and geometry in all layers and all collections.
Ctrl + Shift + Click will hide all curves and geometry in all layers and all collections.
# gsCurveFilter
Layer filter. Hides all geometry and shows all the curves in all layers.
Ctrl + Click will do the same thing, but for all layers and all collections.
NOTE: Holding RMB will open a marking menu with Toggle Always on Top function as well as "Auto-Hide Inactive Curves" function.
Toggle Always on Top function will toggle the Always on Top feature that will show the curve component always on top. The effect is different in different Maya versions
Auto-Hide Inactive Curves will hide all the curve components on all inactive layer collections when switching between collections.
# gsGeoFilter
Layer filter. Hides all curves and shows only geometry.
Ctrl + Click will do the same thing, but for all layers and all collections.
# colorMode
Color mode toggle. Enables colors for each layer and (optionally) UV checker material.
NOTE: Checker pattern can be disabled in the Options menu
# curveGrp0
Curve Layers that are used for organization of the cards and tubes in the scene.
Selected layer (white outline) will be used to store newly created cards.
Holding RMB will open a marking menu with all the functions of current layer.
***
Key Combinations:
Shift + Click: additively select the contents of the layers.
Ctrl + Click: exclusively select the contents of the layer.
Alt + Click: show/hide selected layer.
Ctrl + Shift: show/hide curve component on selected layer.
Ctrl + Alt: show/hide geo component for the selected layer.
Shift + Alt + Click: isolate select the layer.
Shift + Ctrl + Alt + Click: enable Always on Top for each layer (only for Maya 2022+).
***
Layer MMB Dragging:
MMB + Drag: move the contents of one layer to another layer. Combine if target layer has contents.
MMB + Shift + Drag: copy the contents of one layer to another layer. Copy and Add if target layer has contents.
# gsExtractSelected
Extracts (Duplicates) the geometry component from the selected curves:
***
Key Combinations:
Normal click will extract geometry and combine it.
Shift + Click will extract geometry as individual cards.
Ctrl + Click will extract geometry, combine it, open export menu and delete the extracted geo after export.
Shift + Ctrl click will extract geometry, open export menu and delete the extracted geo after export.
# gsExtractAll
Extracts (Duplicates) the geometry component from all layers and collections. Original layers are HIDDEN, NOT deleted:
Last Layer in the current Collection is ignored by default. Can be changed in the options.
Collections with "template" in their name (case insensitive) will be ignored by default. Can be changed in the options.
***
Key Combinations:
Normal click will extract geometry and combine it.
Shift + Click will extract geometry as individual cards grouped by layers.
Ctrl + Click will extract geometry, combine it, open export menu and delete the extracted geo after export.
Shift + Ctrl click will extract geometry, open export menu and delete the extracted geo after export.
# gsSelectCurve
Selects the curve components of the selected Curve Cards/Tubes.
NOTE: Useful during the selection in the outliner.
# gsSelectGeo
Selects the geometry component of the selected Curve Cards/Tubes.
NOTE: Useful for quick assignment of the materials.
# gsSelectGroup
Selects the group component of the selected Curve Cards/Tubes.
NOTE: Useful when you are deleting curves from viewport selection.
# gsGroupCurves
Groups the selected curves and assigns the name from Group Name input field (or default name if empty).
# gsRegroupByLayer
Regroups all the curves based on their layer number, group names and collection names.
Group names can be changed in the Layer Names & Colors menu.
Groups can be colorized if the "Colorize Regrouped Layers" is enabled in the Options menu.
Collections with "template" in their name will be grouped under "CT_Templates". Can be changed in the options.
# gsGroupNameTextField
The name used by the Group Curves function.
If empty, uses the default name.
# gsCustomLayerNamesAndColors
Opens a menu where group names and colors can be changed and stored in a global preset.
# gsTransferAttributes
Transfers attributes from the FIRST selected curve to ALL the other curves in the selection.
NOTE: Shift + Click transfers the attributes from the LAST selected curve to ALL others.
NOTE2: Holding RMB on this button opens a marking menu with Copy-Paste and Filter functionality
# gsTransferUVs
Transfers UVs from the FIRST selected curve to ALL the other curves in the selection.
NOTE: Shift + Click transfers the UVs from the LAST selected curve to ALL others.
NOTE2: Holding RMB on this button opens a marking menu with Copy-Paste and Filter functionality
# gsResetPivot
Resets the pivot on all selected curves to the default position (root CV).
# gsRebuildWithCurrentValue
Rebuild selected curves using current rebuild slider value
# gsResetRebuildSliderRange
Reset rebuild slider range (1 to 50)
# gsDuplicateCurve
Duplicates all the selected curves and selects them.
NOTE: You can select either NURBS curve component, geometry component or group to duplicate.
# gsRandomizeCurve
Opens a window where different attributes of the selected curves can be randomized:
1. Enable the sections of interest and change the parameters.
2. Dragging the sliders in each section enables a PREVIEW of the randomization. Releasing the slider will reset the curves.
3. Click Randomize if you wish to apply the current randomization.
# gsExtendCurve
Lengthens a selected curves based on the Factor slider.
# gsReduceCurve
Shortens the selected curves based on the Factor slider.
# gsSmooth
Smoothes selected curves or curve CVs based on the Factor slider.
NOTE 1: At least 3 CVs should be selected for component smoothing.
NOTE 2: Holding RMB will open a marking menu where you can select a stronger smoothing algorithm.
# mirrorX
Mirrors or Flips all the selected curves on the World X axis.
# mirrorY
Mirrors or Flips all the selected curves on the World Y axis.
# mirrorZ
Mirrors or Flips all the selected curves on the World Z axis.
# gsControlCurve
Adds a Control Curve Deformer to the selected curves. Can be used to adjust groups of curves.
NOTE 1: Should NOT be used to permanently control clumps of cards. Use Bind instead.
# gsApplyControlCurve
Applies the Control Curve Deformer.
Either the Control Curve or any controlled Curves can be selected for this to work.
# gsCurveControlWindow
Opens a Curve Control Window. Contains all the available controls for curves.
# gsUVEditorMain
Opens a UV editor that can be used to setup and adjust UVs on multiple cards.
NOTE 1: Lambert material with PNG, JPG/JPEG or TIF/TIFF (LZW or No Compression) texture file is recommended. TGA (24bit and no RLE) is also supported.
NOTE 2: Make sure to select the curves or the group, not the geo, to adjust the UVs.
NOTE 3: Using default Maya UV editor will break GS CurveTools Cards, Tubes and Bound Groups.
NOTE 4: Default UV editor can be used when custom geometry is used in a Bound Groups.
<!-- ui.py --->
<!-- Curve Control Window Buttons and Frames --->
# gsLayerSelector
Shows the Layer of the selected curve.
Selecting different layer will change the layer of all the selected curves.
# gsColorPicker
Layer/Card Color Picker.
# gsCurveColorPicker
Curve Color Picker.
# selectedObjectName
Selected object name. Editing this field will rename all the selected objects.
# lineWidth
Controls the thickness of the selected curves.
# gsBindAxisAuto
Automatic selection of the bind Axis (recommended).
NOTE: Change to manual X, Y, Z axis if bind operation result is not acceptable.
# AxisFlip
Flips the direction of the bound geometry.
# editOrigObj
Temporarily disables curve bind and shows the original objects.
Used to adjust the objects after the bind.
To add or remove from the Bound group use Unbind.
# selectOriginalCurves
Selects the original curves that were attached to a bind curve.
Allows to edit their attributes without using Unbind or Edit Original Objects
# twistCurveFrame
Advanced twist control graph. Allows for precise twisting of the geometry along the curve. Click to expand.
# Magnitude
Twist multiplier. The larger the values, the more the twist. Default is 0.5.
# gsTwistGraphResetButton
Resets the graph to the default state.
# gsTwistGraphPopOut
Opens a larger graph in a separate window that is synced to the main graph.
# widthLockSwitch
Links/Unlinks the X and Z width sliders.
If linked, the sliders will move as one.
# LengthLock
Locks/Unlocks the length slider.
When Locked the geometry is stretched to the length of the curve and the slider is ignored.
# widthCurveFrame
Advanced width control graph. Allows for precise scaling of the geometry along the curve. Click to expand.
# gsWidthGraphResetButton
Resets the graph to the default state.
# gsWidthGraphPopOut
Opens a larger graph in a separate window that is synced to the main graph.
# profileCurveGraph
Advanced control over the profile of the card. Modifies the profile applied by the Profile slider. Click to expand.
Add or remove points and change them to increase or decrease the Profile value along the curve.
# autoEqualizeSwitchOn
Locks the points horizontally to equal intervals to avoid geometry deformation.
# autoEqualizeSwitchOff
Unlocks the points and allows for the full control.
# equalizeCurveButton
Snaps the points to the equal horizontal intervals.
# gsResetProfileGraphButton
Resets the curve to the default state.
# gsProfileGraphPopOut
Opens a larger graph in a separate window that is synced to the main graph.
# reverseNormals
Reverses the normals on the selected cards/tubes.
# orientToNormalsFrame
Orient selected cards/tubes to the normals of the selected geo.
# gsOrientToNormalsSelectTarget
Set selected mesh as a target for the algorithm.
# orientRefreshViewport
Toggles the viewport update during the alignment process.
Disabling can speed up the process.
# gsOrientToNormals
Starts the alignment process.
Will align the selected cards to the normals of the selected geometry.
# flipUV
Flips the UVs of the card/tube horizontally.
# resetControlSliders
Resets the range of the sliders to the default state.
# UVFrame
Legacy controls for the UVs. Just use the new UV Editor.
# solidifyFrame
Expands controls that control the thickness of the cards/tubes.
# solidify
Toggles the thickness of the geometry.
<!-- Curve Control Window Sliders --->
# lengthDivisions
Change the length divisions of the selected cards/tubes.
# dynamicDivisions
Toggles the dynamic divisions mode.
Dynamic divisions will change the divisions of the cards/tubes based on the length of the curves.
In dynamic divisions mode, L-Div slider will control the density of the divisions, not the fixed divisions count.
# widthDivisions
Change the width divisions of the selected cards/tubes.
# Orientation
Change the orientation of the card/tube around the curve.
# Twist
Smoothly twist the entire geometry card/tube. Twists the tip of the card.
# invTwist
Smoothly twist the entire geometry card. Twists the root of the card.
# Width
Change the width of the selected card.
# Taper
Linearly changes the width of the card/tube along the length of the curve.
# WidthX
Change the width of the tube along the X axis.
# WidthZ
Change the width of the tube along the Z axis.
# Length
Change the length of the attached geometry. Works only when Length Unlock button is checked.
# Offset
Offset the geometry along the curve.
# Profile
Change the profile of the card along the length of the curve uniformly.
# profileSmoothing
Smoothing will smooth the profile transition.
# otherFrame
Other less used options
# curveRefine
Controls the number of "virtual" vertices on the curve. These are the vertices that are used to calculate the geometry deformation.
Zero (0) value will disable the refinement and the geometry will be attached directly to the curve. The fastest option.
Larger refine values means smoother geometry that is a closer fit to the curve.
Only increase past 20 if you need additional precision or if there are any visual glitches with the geometry.
Large refine values can cause significant performance drop, lag and other issues on smaller curve sizes.
Recommended values are:
20 for curves with less than 20 CVs.
0 (disabled) or same as the number of CVs for curves with more than 20 CVs.
# autoRefine
Enables auto-refine for selected curves. Recommended to keep this on.
Manual refinement can be helpful if the geometry deformation is wrong or not precise enough.
# samplingAccuracy
Increases the sampling accuracy of the deformer that attaches the geometry to a curve.
Larger values = more precise geometry fit to a curve and more lag.
# surfaceNormals
Changes the smoothing angle of the normals of the geometry.
# gsIterationsSlider
Controls the number of iterations per card.
# gsMinimumAngle
Controls the target angle difference between the normal of the mesh and the card.
# solidifyThickness
Controls the amount of thickness on the geometry.
# solidifyDivisions
Controls the number of divisions on the solidify extrusion.
# solidifyScaleX
Changes the scale on the X axis.
# solidifyScaleY
Changes the scale on the Y axis.
# solidifyOffset
Controls the offset of the solidify extrusion.
# solidifyNormals
Controls the smoothing angle for normals of the solidify extrusion.
# geometryHighlight
If enabled, selecting the curve will also highlight the geometry component that is attached to that curve.
Works only on GS CurveTools curves and geo.
# curveHighlight
If enabled, selected curves and their components will be additionally highlighted for better visibility.
The curves and components will be in X-Ray mode by default.
Colors and transparency values can be changes in the menu below.
# gsSelectedCVColor
Selected CV highlight color
# gsSelectedCVAlpha
Selected CV highlight transparency (alpha)
# gsDeselectedCVColor
Deselected CV highlight color
# gsDeselectedCVAlpha
Deselected CV highlight transparency (alpha)
# curveVisibility
Toggle selected curves highlight
# gsCurveHighlightColor
Selected curve highlight color
# gsCurveHighlightAlpha
Selected curve highlight transparency (alpha)
# hullVisibility
Toggle hull visibility.
Hull is a line that connects all the CVs on the curve.
# gsHullHighlightColor
Hull highlight color
# gsHullHighlightAlpha
Hull highlight transparency (alpha)
# advancedVisibilityFrame
Better highlights for selected curves and components.
# lazyUpdate
Enables lazy update for selected curves.
Lazy update can slightly increase the performance of the highlight,
however it has some visual drawbacks (curve highlight can fail to update when switching curves in component selection mode)
# alwaysOnTop
Toggles X-Ray (always on top) drawing for highlighted components.
Disabling this defeats the purpose of the advanced visibility, but hey, it's your choice.
# curveDistanceColor
Toggles the distance color effect on the curve highlight.
Distance color darkens the curve color the further it is from the camera.
# cvDistanceColor
Toggles the distance color effect on the CV highlight.
Distance color darkens the CVs color the further it is from the camera.
# hullDistanceColor
Toggles the distance color effect on the hull highlight.
Distance color darkens the hull color the further it is from the camera.
# gsDistanceColorMinValue
Distance color minimum.
This value is the minimum allowed color multiplier for the Distance Color effect.
The lower this value, the darker further parts of the curve will be.
Black at 0.0
Original color at 1.0
# gsDistanceColorMaxValue
Distance color maximum.
This value is the maximum allowed color multiplier for the Distance Color effect.
The higher this value, the brighter closest parts of the curve will be.
Black at 0.0
Original color at 1.0
# CVocclusion
Toggles the experimental CV occlusion mode (hull is affected as well)
When the appropriate mesh name is added to Occluder Mesh input field,
this function will automatically hide CVs and hull lines that are behind this mesh (even in X-Ray mode).
Warning: enabling this mode can negatively impart viewport performance.
# gsSelectOccluderButton
This button adds the selected mesh name to the Occluder Mesh input field.
# gsOccluderMeshName
Type the full path for the occluder mesh here, or use the "Select Occluder" button on the left <-
<!-- Layer Customization --->
# gsGenerateLayerColorGradient
Generate a color gradient for the layer colors.
Rows control the number of Rows to generate.
Left color picker sets the initial color.
Right color picker sets the final color.
# gsRandomizeLayerColors
Generate random colors for the layers.
SatMin controls the minimum allowed saturation.
SatMax controls the maximum allowed saturation.
# gsResetAllLayerColors
Resets all the color swatches to the default color.
# gsGetCurrentSceneLayers
Populates the menu with the names and colors stored in the scene.
# gsSetAsCurrentSceneLayers
Applies the names and colors from the menu to the scene.
# gsLoadGlobalLayerPreset
Load the global names and colors preset to the menu.
NOTE: Don't forget to Set to Scene before closing the menu.
# gsSaveGlobalLayerPreset
Saves the current names and colors from the menu to the global preset.
<!-- UV Editor Window --->
# gsUVSelect
Enables the selection of the UVs.
Drag to draw a box selection.
# gsUVMove
Enables the Move tool.
Move the selected UVs or move individual UVs if nothing is selected.
# gsUVRotate
Enables the Rotation of the selected UVs.
Hold LMB and drag anywhere in the viewport to rotate the selected UVs.
Rotation pivot is the center of the individual unscaled UV.
# gsUVScale
Enables the Scaling of the selected UVs.
Hold LMB and drag in the viewport to scale the card Horizontally of Vertically.
Repeated hotkey click will toggle between Horizontal and Vertical scaling.
# gsUVHorizontalScale
Horizontal scaling mode selector.
# gsUVVerticalScale
Vertical scaling mode selector.
# gsDrawUVs
Enables the UVs Drawing Tool:
1. Select UVs using Selection Mode.
2. Enable the UV Drawing Tool.
3. Draw a UV Rectangle anywhere in the viewport to create/move the UVs there.
# gsHorizontalFlipUV
Flips the selected UVs horizontally.
Flipped UVs have the blue circle indicator inside the root rectangle.
# gsVerticalFlipUV
Flips the selected UVs vertically.
# gsResetUVs
Resets the selected UVs to the default 0,1 rectangle.
# gsSyncSelectionUVs
Syncs selection between UV editor and Maya Viewport.
# gsRandomizeUVs
Randomize selected UV positions between already existing UV positions.
***
Normal click will keep the overall density distribution of the UVs.
This means that if there is one card in one position and twenty cards in the other,
it will keep this distribution of 1 to 20.
***
Shift+Click will ignore the original density distribution
and simply randomize the UVs between the original positions.
# gsFocusUVs
Focuses on the selected UVs or on all the UVs if nothing is selected.
# gsUVIsolateSelect
Hides all the unselected UVs and shows only the selected ones.
# gsUVShowAll
Shows all the hidden UVs.
# UVEditorUseTransforms
Use "Coverage" and "Translate Frame" parameters from place2dTexture node for texture.
Offset is not supported.
Diffuse and Alpha channel MUST have the same coverage and translate frame values.
# UVEditorTransparencyToggle
Enable texture map transparency using Alpha map from Transparency plug in the material node
# UVEditorBGColorPicker
Background Color
# UVEditorGridColorPicker
Grid Color
# UVEditorFrameColorPicker
Frame Color
# UVEditorUVFrameSelectedColorPicker
Selected UV frame color
# UVEditorUVFrameDeselectedColorPicker
Deselected UV frame color
# UVEditorUVCardFillColorPicker
UV frame background color
<!-- Card to Curve Window --->
# gsGeoToCurve_outputTypeSwitch
Controls the output of Geo-to-Curve algorithm
# gsGeoToCurve_generateAuto
Automatically determine the final object type (card or tube) based on the selected geometry.
# gsGeoToCurve_generateCards
Generate cards from selected geometry (one-sided cards or tubes)
# gsGeoToCurve_generateTubes
Generate tubes from selected geometry (one-sided cards or tubes)
# gsGeoToCurve_generateCurves
Generate curves from selected geometry (one-sided cards or tubes)
# gsGeoToCurve_cardType
Controls the type of generated objects (Warp or Extrude)
# gsGeoToCurve_warp
Generate Warp cards or tubes
# gsGeoToCurve_extrude
Generate Extrude cards or tubes
# gsGeoToCurve_matchAttributes
Controls which attributes on the new cards/tubes should be approximated from the original geometry.
NOTE: This process is not perfect and final result can be inaccurate.
# gsGeoToCurve_orientation
Match orientation attribute during the generation process
# gsGeoToCurve_width
Match orientation attribute during the generation process
# gsGeoToCurve_taper
Match taper attribute during the generation process
# gsGeoToCurve_twist
Match twist attribute during the generation process
# gsGeoToCurve_profile
Match profile attribute during the generation process
# gsGeoToCurve_material
Copy material (shader) from the original geometry
# gsGeoToCurve_UVs
Tries to approximate the UVs from the original geometry
NOTE: Matches the bounding box of the UVs. Rotated and deformed UVs are not matched precisely.
# gsGeoToCurve_UVMatchOptions
Controls UV matching behaviour
# gsGeoToCurve_verticalFlip
Vertically flip matched UVs
# gsGeoToCurve_horizontalFlip
Horizontally flip matched UVs
# gsGeoToCurve_reverseCurve
Reverse generated curve direction
Root CV should be generated near the scalp of the model.
Enable or disable if resulting card direction and taper are reversed.
# gsGeoToCurve_convertSelected
Convert selected geometry to cards, tubes or curves based on the selected options.
Newly created procedural objects will be placed in the currently selected layer.
NOTE: if the currently selected layer is hidden (grayed out), the newly created objects will be hidden as well.

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,93 @@
"""
CV Manipulator (highlighting) plug-in entry point for Mac
GS CurveTools License:
This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi)
and can not be copied or distributed without his written permission.
GS CurveTools v1.3.8 Personal
Copyright 2024, George Sladkovsky (Yehor Sladkovskyi)
All Rights Reserved
UI font is Roboto that is licensed under the Apache 2.0 License:
http://www.apache.org/licenses/LICENSE-2.0
Autodesk Maya is a property of Autodesk, Inc:
https://www.autodesk.com/
Social Media and Contact Links:
Discord Server: https://discord.gg/f4DH6HQ
Online Store: https://sladkovsky3d.artstation.com/store
Online Documentation: https://gs-curvetools.readthedocs.io/
Twitch Channel: https://www.twitch.tv/videonomad
YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky
ArtStation Portfolio: https://www.artstation.com/sladkovsky3d
Contact Email: george.sladkovsky@gmail.com
"""
# pylint: disable-all
import sys
try:
from importlib import reload
except ImportError:
from imp import reload
import maya.api.OpenMaya as om
import maya.api.OpenMayaRender as omr
from gs_curvetools.plugins import cv_manip_src # type: ignore
reload(cv_manip_src)
# API parameters
maya_useNewAPI = True
# ------------ Init & UnInit Plugin ------------
def initializePlugin(obj):
plugin = om.MFnPlugin(obj, "GeorgeSladkovsky", "1.3", "Any")
try:
plugin.registerNode(
"GSCT_CurveTools_DrawManagerNode",
cv_manip_src.DrawManagerNode.id,
cv_manip_src.DrawManagerNode.creator,
cv_manip_src.DrawManagerNode.initialize,
om.MPxNode.kLocatorNode,
cv_manip_src.DrawManagerNode.drawDbClassification,
)
except BaseException:
sys.stderr.write("Failed to register node\n")
raise
try:
omr.MDrawRegistry.registerDrawOverrideCreator(
cv_manip_src.DrawManagerNode.drawDbClassification,
cv_manip_src.DrawManagerNode.drawRegistrantId,
cv_manip_src.DrawOverride.creator,
)
except BaseException:
sys.stderr.write("Failed to register override\n")
raise
def uninitializePlugin(obj):
om.MMessage.removeCallbacks(cv_manip_src.CALLBACK_IDS)
cv_manip_src.CALLBACK_IDS = []
plugin = om.MFnPlugin(obj)
try:
plugin.deregisterNode(cv_manip_src.DrawManagerNode.id)
except BaseException:
sys.stderr.write("Failed to deregister node\n")
raise
try:
omr.MDrawRegistry.deregisterGeometryOverrideCreator(
cv_manip_src.DrawManagerNode.drawDbClassification, cv_manip_src.DrawManagerNode.drawRegistrantId
)
except BaseException:
sys.stderr.write("Failed to deregister override\n")
raise

View File

@@ -0,0 +1,206 @@
"""
License:
This collection of code named GS Toolbox is a property of George Sladkovsky (Yehor Sladkovskyi)
and can not be copied or distributed without his written permission.
GS Toolbox v1.2.2 Personal Edition
Copyright 2025, George Sladkovsky (Yehor Sladkovskyi)
All Rights Reserved
Autodesk Maya is a property of Autodesk, Inc.
Social Media and Contact Links:
Discord Server: https://discord.gg/f4DH6HQ
Online Store: https://sladkovsky3d.artstation.com/store
Online Documentation: https://gs-toolbox.readthedocs.io/
Twitch Channel: https://www.twitch.tv/videonomad
YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky
ArtStation Portfolio: https://www.artstation.com/sladkovsky3d
Contact Email: george.sladkovsky@gmail.com
"""
https://www.artstation.com/marketplace-product-eula
Marketplace Product & Services Agreement
End User Agreement
This Marketplace End User Agreement applies to all downloadable products and professional services (e.g. mentorships, personal training, portfolio reviews) sold via the ArtStation Marketplace, unless a custom agreement or license is provided by the seller.
The EUA is an agreement between the buyer and the seller providing the goods or services.
PLEASE READ THIS DOCUMENT CAREFULLY. IT SIGNIFICANTLY ALTERS YOUR LEGAL RIGHTS AND REMEDIES.
BY CLICKING “I AGREE” OR DOWNLOADING OR USING THE DIGITAL PRODUCT OR RECEIVING THE PROFESSIONAL SERVICES TO WHICH THIS AGREEMENT RELATES YOU ACCEPT ALL OF THIS AGREEMENTS TERMS, INCLUDING THE DISCLAIMERS OF WARRANTIES AND LIMITATIONS ON DAMAGES, USE AND TRANSFERABILITY. IF YOU DO NOT ACCEPT THIS AGREEMENTS TERMS, DO NOT DOWNLOAD, INSTALL OR USE THE DIGITAL PRODUCT OR RECEIVE OR USE THE PROFESSIONAL SERVICES.
This end-user agreement (“Agreement”) is a legally binding agreement between you, the licensee and customer (“you” or “your”), and the provider (“we” or “us” or “our”) of the digital products (“Products”) or instructional, training, mentorship or other professional service packages (“Professional Services”) that you purchase through the ArtStation Marketplace, regarding your rights and obligations regarding those Products and Professional Services.
1. Your Status
In this Agreement, “you” means the person or entity acquiring rights in the Products or purchasing Professional Services. That may be a natural person, or a corporate or business entity or organization.
(a) If you are a natural person then you must be, and you confirm that you are, at least 13 years old. If you are between 13 years and the age of majority in your jurisdiction of residence, you confirm that your parent or legal guardian has reviewed and agrees to this Agreement and is happy for you to access and use the Product or receive the Professional Services.
(b) If you are a corporate entity then: (i) the rights granted under this Agreement are granted to that entity; (ii) you represent and warrant that the individual completing and accepting this Agreement is an authorized your representative and has the authority to legally bind that you to the Agreement; and (iii) to the extent that one or more of your employees are granted any rights in the Product or to receive Professional Services under this Agreement, you will ensure that your employees comply with this Agreement and you will be responsible and liable for any breach of this Agreement by any employee.
2. ArtStation
ArtStation is a division of Epic Games, Inc., You acknowledge and agree that Epic is a third-party beneficiary of this Agreement and therefore will be entitled to directly enforce and rely upon any provision in this Agreement that confers a benefit on, or rights in favour of, Epic. In addition, you authorize Epic to act as your authorized representative to file a lawsuit or other formal action against a licensor in a court or with any other governmental authority if Epic knows or suspects that a licensor breached any representations or warranties under this Agreement. The foregoing authorization is nonexclusive, and Epic shall be under no obligation to pursue any claim. Epic will not initiate any such action on your behalf without first consulting with and obtaining your approval.
Products
The following sections 3 through 9 apply to any Products you acquire from us through the ArtStation Marketplace:
3. Product Licence
Subject to this Agreements terms and conditions, we hereby grant you a limited, non-exclusive, worldwide, non-transferable right and licence to (which will be perpetual unless the licence terminates as set out in this Agreement): (a) download the Product; and (b) copy and use the Product. We reserve all rights not expressly granted to you under this Agreement.
4. Licence Scope and Restrictions
(a) Tutorials
You are purchasing ONE licence to create ONE copy of the Product for use by you only (or, if you are a corporate entity, for use by a single authorized employee).
If this Product is bundled with a stock digital asset then you receive a limited personal use licence regarding that stock digital asset, and you may use that stock digital asset for your personal use only. You will not use that stock digital asset in any commercial manner unless you purchase a separate commercial licence.
(b) Installable Tools
You may purchase one or more licences for the Product. A single licence allows you to install the Product on a single computer at a time for use by a single authorized user. If you are a corporate entity and the authorized employee completing the transaction on your behalf purchases multiple licences, you may choose to store the Product on a single server or shared hard drive for use by a single authorized employee at a time for each licence purchased.
Provided that you comply with the restrictions on users set out above, you may use the Product on an unlimited number of projects.
(c) Stock Assets
Subject to the restrictions set out in this Agreement, you may copy, use, modify, adapt, translate, distribute, publicly display, transmit, broadcast, and create derivative works from the Product in works you create (“Works”), which may include things like films, videos, multi-media projects, computer games, models, images, publications, broadcasts, documents, and presentations.
If you are a corporate entity, you may make the Product available for use by your employees in accordance with this Agreement (for example, by storing the Product on a network server).
You may only share the Product with external people or entities where:
- You are collaborating with the external parties in the creation of your Work and you need to share the Product for that purpose, provided that any external party that receives the Product may only use it in your Work and must secure and limit access to the Product for that purpose;
- You are working as a contractor for a client in the creation of a Work and need to share the Product with your client, or any external parties working with your client, provided that your client and any such external parties may use the Product only for your clients Work, and all parties secure and limit access to the Product for that purpose.
For any other use of the Product by any other party, that party must purchase a licence to the Product.
In addition to any other restrictions in this Agreement, you will not:
- publish, sell, license, offer or make available for sale or licensing, or otherwise distribute the Product except as part of a Work or through a form of sharing that is authorized in this Agreement; or
- publish, distribute or make available the Product through any online clearinghouse platform.
FURTHER SPECIFIC TERMS
In addition to the restrictions set out above, the following terms and conditions apply to the following forms of commercial licences for the Product:
Standard Commercial Licence
If you have purchased a Standard Commercial licence then you may exercise your rights under that licence:
- for personal use on an unlimited number of personal projects that are not used or distributed in any commercial manner; and
- respect to one commercial Work, with up to a maximum of, as applicable, 2,000 sales of the Work or 20,000 monthly views of the Work.
Extended Commercial Licence
If you have purchased an Extended Commercial licence then you may exercise your rights under that licence:
- for personal use on an unlimited number of personal projects that are not used or distributed in any commercial manner; and
- with respect to any number of commercial Works, with no limit on sales or views.
5. Additional Restrictions
Except as expressly permitted under this Agreement, you will not:
(a) make any copy of the Product except for archival or backup purposes;
(b) circumvent or disable any access control technology, security device, procedure, protocol, or technological protection mechanism that may be included or established in or as part of the Product;
(c) hack, reverse engineer, decompile, disassemble, modify or create derivative works of the Product or any part of the Product;
(d) publish, sell distribute or otherwise make the Product available to others to use, download or copy;
(e) transfer or sub-license the Product or any rights under this Agreement to any third party, whether voluntarily or by operation of law;
(f) use the Product for any purpose that may be defamatory, threatening, abusive, harmful or invasive of anyones privacy, or that may otherwise violate any law or give rise to civil or other liability;
(g) misrepresent yourself as the creator or owner of the Property;
(h) remove or modify any proprietary notice, symbol or label in or on the Product;
(i) directly or indirectly assist, facilitate or encourage any third party to carry on any activity prohibited by this Agreement.
6. Proprietary Rights
The Product is protected by copyright laws and international copyright treaties, as well as other intellectual property laws and treaties. You are licensing the Product and the right to access, install and use the Product in accordance with this Agreement, not buying the Product. As between you and us, we own all right, title and interest in and to the Product, and you are not acquiring any ownership of or rights in the Product except the limited rights granted under this Agreement.
7. No Epic Support
You acknowledge and agree that you are licensing the Product from us (the Provider), not from Epic, and that Epic has no obligation to support the Product.
8. Interruptions and Errors
Your use of the Product might be interrupted and might not be free of errors.
9. Updates
We have no obligation to update the Product.
Professional Services
The following sections 10 and 11 apply to any Professional Services you purchase from us through the ArtStation Marketplace:
10. Provision of Professional Services
We will provide the Professional Services directly to you and, subject to this Agreement, will assume all responsibility for all aspects of the Professional Services. We represent and warrant that we have the right to offer and provide the Professional Services and that we have appropriate qualifications and experience to provide the Professional Services.
11. Epic is not Involved
You acknowledge and agree that:
(a) Epic is only a provider of the online ArtStation Marketplace where you purchased the Professional Services, and does not provide or exercise any control or oversight over us or the Professional Services, and is not responsible for us or the Professional Services or any shortcomings in them, including any damages, losses or legal issues caused by us or the Professional Services;
(b) this Agreement (and any dispute under it) is an agreement between us and you only, and not with Epic, and Epic is not a party to this Agreement;
(c) we are not Epics employee, agent or subcontractor;
(d) Epic does not have any obligation to attempt to resolve any dispute between us and you; and
(e) we will provide the Professional Services directly to you, and we (and not Epic) are solely responsible for the Professional Services, and Epic has no obligation or liability to you with respect to the Professional Services.
Both Products and Services
The following sections 12 through 25 apply to all Products or Services you purchase from us through the ArtStation Marketplace:
12. Disclaimer
ANY PRODUCTS OR PROFESSIONAL SERVICES ARE PROVIDED ON AN “AS IS” AND “AS AVAILABLE” BASIS, WITHOUT ANY REPRESENTATIONS, WARRANTIES OR CONDITIONS OF ANY KIND.
TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW WE DISCLAIM, AND YOU WAIVE (WITH REGARD TO US AND ALSO TO EPIC, ITS AFFILIATES, AND ITS AND THEIR LICENSORS AND SERVICE PROVIDERS (COLLECTIVELY, THE “EPIC PARTIES”), ALL TERMS, CONDITIONS, GUARANTEES, REPRESENTATIONS AND WARRANTIES (EXPRESS, IMPLIED, STATUTORY AND OTHERWISE), IN RESPECT OF THE PRODUCTS AND PROFESSIONAL SERVICES, INCLUDING THOSE OF MERCHANTABILITY, NON-INFRINGEMENT, TITLE, QUALITY AND FITNESS FOR A PARTICULAR PURPOSE.
NEITHER WE NOR ANY OF THE EPIC PARTIES REPRESENT OR WARRANT THAT: (A) ANY PRODUCT OR PROFESSIONAL SERVICE IS ACCURATE, COMPLETE, RELIABLE, CURRENT OR ERROR-FREE; (B) ANY PRODUCT OR PROFESSIONAL SERVICE WILL MEET YOUR REQUIREMENTS OR EXPECTATIONS; (C) ANY PRODUCT OR PROFESSIONAL SERVICES IS FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS; OR (D) ANY DEFECTS IN ANY PRODUCT OR PROFESSIONAL SERVICE WILL BE CORRECTED.
13. Exclusion and Limitation of Liability
(a) YOU DOWNLOAD, INSTALL AND OTHERWISE USE ALL PRODUCTS, AND RECEIVE AND USE ALL PROFESSIONAL SERVICES, AT YOUR OWN RISK. YOU AGREE TO, AND HEREBY DO:
(i) WAIVE ANY CLAIMS THAT YOU MAY HAVE AGAINST US OR THE EPIC PARTIES OR OUR RESPECTIVE DIRECTORS, OFFICERS, EMPLOYEES, AGENTS, REPRESENTATIVES, LICENSORS, SUCCESSORS AND ASSIGNS (COLLECTIVELY THE “RELEASEES”) ARISING FROM OR RELATING TO ANY PRODUCTS OR PROFESSIONAL SERVICES, AND
(ii) RELEASE THE RELEASEES FROM ANY LIABILITY FOR ANY LOSS, DAMAGE, EXPENSE OR INJURY ARISING FROM OR RELATING TO YOUR USE OF ANY PRODUCT OR PROFESSIONAL SERVICE, WHETHER ARISING IN TORT (INCLUDING NEGLIGENCE), CONTRACT OR OTHERWISE, EVEN IF THE RELEASEES ARE EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH LOSS, INJURY OR DAMAGE AND EVEN IF THAT LOSS, INJURY OR DAMAGE IS FORESEEABLE.
(b) NEITHER WE NOR THE EPIC PARTIES WILL BE LIABLE FOR ANY LOSSES, DAMAGES, CLAIMS OR EXPENSES THAT CONSTITUTE: (I) LOSS OF INTEREST, PROFIT, BUSINESS, CUSTOMERS OR REVENUE; (II) BUSINESS INTERRUPTIONS; (III) COST OF REPLACEMENT PRODUCTS OR SERVICES; OR (IV) LOSS OF OR DAMAGE TO REPUTATION OR GOODWILL.
(c) NEITHER WE NOR THE EPIC PARTIES WILL BE LIABLE FOR ANY LOSSES, DAMAGES, CLAIMS OR EXPENSES THAT CONSTITUTE INCIDENTAL, CONSEQUENTIAL, SPECIAL, PUNITIVE, EXEMPLARY, MULTIPLE OR INDIRECT DAMAGES, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, DAMAGES, CLAIMS OR EXPENSES.
(d) MAXIMUM LIABILITY: IF, DESPITE THE LIMITATIONS SET OUT ABOVE, WE OR ANY EPIC PARTY BECOME LIABLE TO YOU IN RESPECT OF ANY PRODUCT OR PROFESSIONAL SERVICE OR OTHERWISE UNDER THIS AGREEMENT, THE ENTIRE CUMULATIVE LIABILITY OF US AND THE EPIC PARTIES, AND YOUR EXCLUSIVE AND CUMULATIVE REMEDY, FOR ANY DAMAGES (REGARDLESS OF THE CAUSE OR FORM OR ACTION), WILL BE LIMITED TO CAD$10.
14. Indemnity
As a condition of your use of any Product or any Professional Services, you agree to hold harmless and indemnify the Releasees from any liability for any loss or damage to any third party resulting from your access to, installation or use of the Product or your receipt and use of the Professional Services.
15. Term and Termination
This Agreement is effective until terminated. Your rights under this Agreement will terminate automatically without notice if: (a) you breach any terms of this Agreement; or (b) you do not complete payment for the Product or Professional Services, or any payment you make is refunded, reversed or cancelled for any reason. Upon this Agreements termination, you will cease all use of the Product and destroy all copies, full or partial, of the Product in your possession. Sections 11 through 25 will survive the termination of this Agreement.
16. Compliance with Laws
You will comply with all applicable laws when using any Product or Professional Services (including intellectual property and export control laws).
17. Entire Agreement
This Agreement supersedes all prior agreements of the parties regarding the Product or Professional Services, and constitutes the whole agreement with respect to the Product or Professional Services.
18. Disputes
If you have any concerns about the Product or Professional Services, please contact us through our ArtStation Marketplace account and we will work with you to try to resolve the issue. You acknowledge and agree that any such dispute is between you and us, and that Epic will not be involved in the dispute and has no obligation to try to resolve the dispute.
19. Persons Bound
This Agreement will enure to the benefit of and be binding upon the parties and their heirs, executors, administrators, legal representatives, lawful successors and permitted assigns.
20. Assignment
We may assign this Agreement without notice to you. You may not assign this Agreement or any of your rights under it without our prior written consent, which we will not withhold unreasonably.
21. Waiver
No waiver, delay, or failure to act by us regarding any particular default or omission will prejudice or impair any of our rights or remedies regarding that or any subsequent default or omission that are not expressly waived in writing.
22. Applicable Law and Jurisdiction
You agree that this Agreement will be deemed to have been made and executed in the State of North Carolina, U.S.A., and any dispute will be resolved in accordance with the laws of North Carolina, excluding that body of law related to choice of laws, and of the United States of America. Any action or proceeding brought to enforce the terms of this Agreement or to adjudicate any dispute must be brought in the Superior Court of Wake County, State of North Carolina or the United States District Court for the Eastern District of North Carolina. You agree to the exclusive jurisdiction and venue of these courts. You waive any claim of inconvenient forum and any right to a jury trial. The Convention on Contracts for the International Sale of Goods will not apply. Any law or regulation which provides that the language of a contract shall be construed against the drafter will not apply to this Agreement.
23. Legal Effect
This Agreement describes certain legal rights. You may have other rights under the laws of your country. This Agreement does not change your rights under the laws of your country if the laws of your country do not permit it to do so.
24. Interpretation
In this Agreement, "we", "us", and "our" refer to the licensor of the Product alone and never refer to the combination of you and that licensor (that combination is referred to as "the parties"), or the combination of you or the licensor with Epic.
25. Artificial Intelligence
For purposes of this Agreement, “Generative AI Programs” means artificial intelligence, machine learning, deep learning, neural networks, or similar technologies designed to automate the generation of or aid in the creation of new content, including but not limited to audio, visual, or text-based content.
We (the licensor of the Product) represent and warrant that where the Product was created using Generative AI Programs, we have applied the “CreatedWithAI” tag. Under this Agreement, a Product is considered to be created using Generative AI Programs where a material portion of a Product is generated with Generative AI Programs, whether characters, backgrounds, or other material elements. A Product is not considered to be created using Generative AI Programs merely for use of features that solely operate on a Product (e.g., AI-based upscaling or content-aware fill).

View File

@@ -0,0 +1,754 @@
<!--
Tooltips are added in format:
# Widget Name
Tooltip
-->
<!-- Options Menu -->
# gsUpdateSelectionSets
Update selection groups from the available sets in Maya scene.
Can fix issues like groups available in Maya Outliner but not showing properly.
# gsClearSelectionSets
Clear all selection groups. The objects will not be deleted.
# gsSetsHiddenInOutliner
Selection groups will not show in the outliner as sets.
# gsClearCustomMaterials
Clears the custom material swatches from materials. The materials will not be deleted from the scene.
# gsBoolMerged
Boolean objects are merged for more effective Boolean operation. Faster. Instances are not supported.
# gsBoolOptimized
Default Boolean operation. Instances are supported.
# gsBoolAdvanced
Each object has a separate Boolean operation.
Attributes (Boolean Active, Boolean Operation, Boolean Classification) can be edited on individual objects.
# gsBoolWireframe
When performing Boolean operation the cutter will be in wireframe mode.
# gsBoolConvert
Convert Boolean instances automatically.
# gsBoolGroup
Boolean objects will be stored in a separate group in the outliner.
# gsBoolReverseSelectionOrder
Reverses the order in which boolean objects need to be selected.
By default: the first selected object is the base object and all the next - cutters.
When checked: the last object is the base object and all the previous - cutters.
# gsAdditiveCrease
Creasing operations will not overwrite each other.
# gsCreasePerimeter
Crease operation will crease only the perimeter of a face selection (Hold Shift to crease everything).
# gsTooManyInstances
Check the number of instances created.
# gsTooManyEdges
Warns if there are too many edges selected for some operations (Interpolation, Straighten, Smooth).
# gsEnableTooltips
Toggle tooltips visibility.
# gsResetToDefaults
Resets everything to the default values. Can help fix issues with the UI or functions.
<!-- Main menu -->
# gsWireframeToggle
Toggles the wireframe mode for the selected objects.
# gsReferenceToggle
Toggles the reference mode for the selected objects.
Reference mode will prevent viewport selection.
Selecting the referenced object is possible from the Outliner.
# gsTransformConstraint
Toggles the transform constraint.
This locks the movement of the selected component to either Edge (default) or Surface.
- Hold RMB to switch between the constraint types.
# gsSelectEdges
Select the edges on the currently selected object based on the Angle slider.
Hold RMB for alternative selection types:
- Select Border Edges - will select the border edges of the selected objects.
- Select N-Gons - will select the faces with more than 4 vertices of the selected objects.
- Select Hard Edges - will select hard edges (based on normals) of the selected objects.
# gsDotSelect
Continues the selection in a "dot" pattern. The selection will respect the selected/deselected components on the object and will continue this pattern.
- LMB Click - Add the next appropriate component in a pattern to the selection list.
- Shift + Click - Select all the pattern-appropriate components in a current loop.
# gsSelectConstraint
Toggles the selection constraint.
By default 1 degree angle tolerance is activated.
The selection will be automatically expanded based on this angle tolerance.
Hold RMB to switch between options:
- Select by Angle - will automatically expand the selection based on the angle tolerance.
- Auto Camera-Based Selection - will respect the camera based view and select only directly visible components.
- Angle + Auto Camera-Based - a combination of both options.
- Angle Selectors - Determines the angle tolerance for the Select by Angle option.
# quickSet0
Quick selection groups (sets) menu.
LMB Click:
- If the selection group is empty (no color) - clicking on it with the objects (or components) selected will add this object (or component) to this group.
- If the selection group is not empty (orange color) - clicking on this selection group will select the objects (or components) in this group.
MMB drag:
From a non-empty group to empty group - move the objects and components to the empty group.
From a non-empty group to a non empty group with modifiers:
- Shift + Ctrl: Additive.
- Shift + Alt: Subtractive.
- Ctrl + Alt: Intersection.
LMB Click modifiers:
- Shift + Ctrl: Additive. Will combine the group with the selected objects. Basically the same as Add Selection to Group from the marking menu.
- Shift + Alt: Subtractive. Will subtract the selection from the group. Most useful for components.
- Ctrl + Alt: Intersection. Will intersect the selection with the group and leave only intersecting (same) components.
RMB Hold:
- Merge To This Group - merge all the selected objects (or components) to this set (group). If the objects (components) are in other groups, remove them from those groups.
- Toggle Group Reference (green color) - toggle the reference mode on the current group. Reference mode prevents viewport selection.
- Toggle Group Visibility (light gray) - show/hide objects (components) from the current group.
- Add Selection To Group - adds the currently selected objects (or components) to the group. Does not remove them from other groups.
- Remove Selection From Group - remove currently selected objects (or components) from this group. Objects are not deleted.
- Clear Group - clears the current group from any objects and components making it empty. Objects are not deleted.
# gsCrease
Creases the edges based on the selection. Faces, edges and vertex selection is compatible.
- Click - creases the directly selected edges, adjacent edges in vert selection and face selection border.
- Shift + Click - creases all the edges in a face selection, not only the border.
# gsUnCrease
Removes the creases on the edges based on the selection. Faces, edges and vertex selection is compatible.
- Click - removes the creases on the directly selected edges, adjacent edges in vert selection and face selection border.
- Shift + Click - removes the creases for all the edges in a face selection, not only the border.
# gsSubDLevel
Sets the smooth mesh preview (press 3 to activate and 1 to deactivate) subdivision level based on the SubD slider value.
# gsSubDNumber
Indicates the current subdivision level on a selected object. Clicking will also set the subdivision level.
# gsCreasePlus
Creases all the edges on the selected objects based on the Angle slider.
Additionally, it applies the subdivision preview level based on the SubD slider.
Holding RMB will open a marking menu:
- Create Crease Sets from Mesh - will create a special selection set in the outliner that will hold all the creased edges from the selected object. One set will be created for each distinct crease level.
- Bake Crease Sets to Mesh - will delete the special crease sets from the outliner and apply them to the mesh.
- Convert Creases to Bevels - will convert all the creases on the selected objects to bevels. Crease level is NOT respected in this operation.
# gsUnCreasePlus
Removes the creases from all the edges on all the selected objects.
# gsBevelPlus
Bevels the edges on the selected objects based on the Angle slider tolerance.
Hold RMB will open the marking menu:
- Chamfer checkbox will toggle the chamfering of the beveled edges.
- Segments will select the initial segment number on the bevel.
# gsAngleButton
The Angle slider sets the angle tolerance for the Select Edges, Crease+ and Bevel+ functions.
Clicking on this button will enable interactive angle highlight mode.
This mode will highlight the edges that will be affected by the Select Edges, Crease+ and Bevel+ functions.
Dragging the slider in this mode will change the edge highlight based on the angle.
The highlighted edges are not selected by default.
# gsAngleSlider
The Angle slider sets the angle tolerance for the Select Edges, Crease+ and Bevel+ functions.
# gsSubD
The SubD slider sets the smooth mesh preview subdivision level when using Crease+ and Bevel+ functions.
Clicking on this button will enable the dynamic subdivision mode.
Dragging the slider in this mode will dynamically change the subdivision levels for the smooth mesh preview for the selected objects.
# gsSubDSlider
The SubD slider sets the smooth mesh preview subdivision level when using Crease+ and Bevel+ functions.
# gsCrs
The Crease slider sets the crease level that will be used by Crease and Crease+ functions.
Clicking on this button will enable the dynamic creasing mode.
Dragging the slider in this mode will change the crease value of the selected edges dynamically.
# gsCrsSlider
The Crease slider sets the crease level that will be used by Crease and Crease+ functions.
<!-- Mirror/arrays switch -->
# gsNormalMirrorSwitch
Switch to mirror functions.
# gsLinearArraySwitch
Switch to Linear Array functions.
Linear array creates an array from the selected object adding a curve controller.
Array Control Window is used to control the arrays.
# gsRadialArraySwitch
Switch to Radial Array functions.
Radial array creates a radial array object from the selected mesh.
Array Control Window is used to control the arrays.
<!-- Mirror functions -->
# gsMirrorX
Mirror the selected objects on the X axis.
# gsMirrorY
Mirror the selected objects on the Y axis.
# gsMirrorZ
Mirror the selected objects on the Z axis.
# gsMirrorMinusX
Mirror the selected objects on the -X axis.
# gsMirrorMinusY
Mirror the selected objects on the -Y axis.
# gsMirrorMinusZ
Mirror the selected objects on the -Z axis.
<!-- Mirror modifiers -->
# gsMirrorRadio
Perform a normal mirror.
# gsFlipRadio
Flip the selected object on the axis.
# gsInstanceRadio
Instantiate the selected object and flip it on the axis.
# gsWorldRadio
Mirror operations will be performed on the world (scene) axis.
# gsObjectRadio
Mirror operations will be performed on the object(s) axis (based on pivot).
# gsBoundingBoxRadio
Mirror operations will be performed based on the bounding box of an object.
# gsMergeRadio
Vertices will be merged after mirror based on the tolerance selected in the Marking Menu:
- Auto - will select the merge tolerance based on the size of an object.
- Change Merge Threshold - manually set the merge threshold.
# gsBridgeRadio
The object will be bridged after the mirror operation is complete.
# gsNothingRadio
Nothing will be done to the object vertices after the mirror operation.
# gsMirrorCut
Will cut the geometry during mirror operation. The cutting plane is the axis plane.
# gsMirrorDelete
Will delete the geometry based on the selected axis. The cutting plane is the axis plane.
<!-- Linear Arrays -->
# gsArrayUniform
The resulting array will be a uniform array attached to a curve that conforms to a curve (if linear) or radially distributed (if radial) array.
No deformation to the individual objects.
# gsArrayDeformed
The resulting array will be an array of meshes deformed to match the curvature of the control curve (if linear) or circular shape (if radial).
# gsLinearArrayX
Create an array on the X axis. If the custom curve is selected - array will be created on that curve.
Editing the initial object will also edit the array dynamically.
# gsLinearArrayY
Create an array on the Y axis. If the custom curve is selected - array will be created on that curve.
Editing the initial object will also edit the array dynamically.
# gsLinearArrayZ
Create an array on the Z axis. If the custom curve is selected - array will be created on that curve.
Editing the initial object will also edit the array dynamically.
# gsLinearArrayMinusX
Create an array on the -X axis. If the custom curve is selected - array will be created on that curve.
Editing the initial object will also edit the array dynamically.
# gsLinearArrayMinusY
Create an array on the -Y axis. If the custom curve is selected - array will be created on that curve.
Editing the initial object will also edit the array dynamically.
# gsLinearArrayMinusZ
Create an array on the -Z axis. If the custom curve is selected - array will be created on that curve.
Editing the initial object will also edit the array dynamically.
<!-- Radial Arrays -->
# gsRadialArrayXY
Create a radial array from the selected object on the XY plane.
Editing the initial object will also edit the array dynamically.
# gsRadialArrayYZ
Create a radial array from the selected object on the YZ plane.
Editing the initial object will also edit the array dynamically.
# gsRadialArrayZX
Create a radial array from the selected object on the ZX plane.
Editing the initial object will also edit the array dynamically.
# gsArrayAdd
Add the selected object to the selected array.
Select the object and the array and click the button.
# gsArrayRemove
Remove the selected object from an array.
Select the initial object and the array and click the button.
# gsArrayCalcRotation
Calculate the rotation of the individual objects in the linear and radial uniform arrays. Deformed arrays are not affected.
# gsArrayShowOriginal
Show/Hide the initial object. Editing this object will edit the array dynamically.
# gsMainCopiesSlider
Controls the number of copies that the arrayed object has.
# gsApplyArray
Apply the selected array, removing all the construction history and dynamic features.
# gsArrayControlWindow
Open the Array Control Window. This window holds buttons and sliders that will control arrays created by GS Toolbox.
<!-- Instancing and Utilities -->
# gsInstanceButton
Create instances of an object. The number of instances is based on the number in the field.
Instances are offset based on the bounding box by default.
Offset and axis of offset are customizable by holding RMB on the button and selecting relevant options.
Hold RMB for marking menu:
- X,Y,Z etc. - selecting an axis will instantiate and offset the object based on this axis.
***
- Instance - will create an instance of an object.
- Copy - will create a simple copy of an object.
- Copy with History - will create copies of the object with all the relevant history nodes connected.
***
- No Offset - will instantiate (or copy) the objects without offset.
- Offset - the instantiated objects will be offset based on the bounding box of the object + selected offset from the menu [].
- Offset + Gap - additional small gap will be added to the instantiated objects in addition to the offset.
***
- Randomize Transforms - will open a randomization window which allows to interactively randomize Transform, Rotate and Scale of the selected objects.
This can be useful in combination with Instancing or Snap to Poly (multiple).
# gsInstanceField
Instance number. Controls the number of instances that Instance button creates.
# gsInstancePlus
Enabling Instance+ on the selected object will create a special instance of that object in the same place as the original.
Instance plus consists of the viewport selectable part (the original object in wireframe mode) and instance part.
This instance supports Mirroring, Booleans, Solidify and Delete Node commands (through GS Toolbox):
To apply Mirror, Solidify or Delete Node to the instantiated part - switch to "Inst" toggle. Inst toggle will highlight the functions that will be applied to the instance part.
Mirroring the instance (using inst toggle) will allow to have a interactive procedural mirror that will work with poly modeling and most of the functions.
Booleans can be applied to the original object (it will be in wireframe mode) and combined with mirror this will allow for interactive mirrored Booleans.
Solidify can be applied to the one-sided Instance+ object to create procedural geometry strips with thickness.
Delete Node on instance will undo (remove nodes) the last procedural modifier (Mirror, Solidify).
Apply Inst+ button will remove the history and return the instance as a normal object for further editing.
# gsInstancePlusApply
Will delete the history on the instance plus object, clean up and return the instance mesh as a regular mesh for further editing.
# gsInstancePlusMeshToggle
Will switch the Mirror, Solidify and Delete node commands back to the original functionality.
# gsInstancePlusInstToggle
Will switch the Mirror, Solidify and Delete Node commands to the instance mode, essentially applying them to the instance part of the Instance Plus object.
# gsStoreEdits
Store Edits will store the polygonal edits on the object (poly tweaks) allowing the use of the "Delete Node" command to undo edits on the object.
Every vert, edge and poly edit can be stored, essentially allowing to undo any edit. Multiple edits can be stored in one "chunk" and it will be undone in one "Delete Node" click.
When the Store Edits button is highlighted (orange, no white outline) this means there are unstored edits on the selected object.
When the Store Edits button is highlighted (orange WITH white outline) this means the object is not initialized for edit storage, and the button should be clicked to initialize it.
# gsUndoEdit
Normal Click - will delete the last history node applied to the current object, undoing the edits (including polygon edits).
Shift + Click - will delete the selected node only (using Select button from Attribute Editor).
# gsSnapshot
Will store a snapshot of the object in a special group in the outliner. The snapshot will respect the transformations of the original object.
Normal Click - will store the snapshot (with history). The original object is not modified.
Shift + Click - will store a snapshot (with history). The original object will have its construction history deleted.
# gsExtractSnapshot
Extracts the selected snapshot from the group.
Normal Click - duplicate the snapshot from the group (the original snapshot stays in the group).
Shift + Click - extract the snapshot from the group (removing it from the group).
# gsSnapToPoly
Snaps selected polygon objects to the selected faces.
Selection order sensitive. If multiple polygonal objects are selected, will create a repeating pattern from the selection based on the selection order and number of faces.
Hold RMB for marking menu:
- Simple Snap will duplicate and snap the objects to the mesh.
- Instance + Snap will instantiate the objects before snapping. Original objects can be edited to edit all the instances on the mesh after snap.
- Pre-Duplication will ensure that the original objects are left behind for editing purposes.
Normal Click will snap the objects to the mesh.
Shift + Click will attempt to center the pivots on the objects (might not work in some cases).
# gsSolidify
Will add thickness to the mesh (extrude). Mainly used in combination with Instance+ to achieve the procedural ribbon mesh with thickness effect.
Shift + Click - will add the thicknes to the other side of the mesh.
Hold RMB for additional options:
- Local translate - will use local translate for thickness. Might not have the perfect uniform thickness.
- Thickness - will use thickness for extrusion. Might have visual artifacts on the sharp edges.
# gsStraighten
Will straighten selected edges (only edges). Multiple edge groups can be selected at a time (not connected edges).
Normal Click - will straighten the edges projecting them to the straight line (no equalization).
Shift + Click - will straighten the edges and equalize them, making the edges the same length.
Hold RMB for additional options:
- Local X, Y, Z - will straighten the edges on the local axis (object space). Works for verts, edges and faces.
- World X, Y, Z - will straighten the edges based on the world axis (world space). Works for verts, edges and faces.
Local and World straightening also works for verts and faces, not only edges.
# gsInterpolate
Will interpolate selected edges to an arc that is made from first, last and middle vert of the selected edge group.
Moving those key edges will change the shape of the arc.
Edges will always be made equal length in the process.
Multiple edge groups can be selected (unconnected edges).
# gsSmooth
Will smooth the selected edges based on the smoothing value (on the left).
# gsSmoothField
Smooth strength. Controls the smoothing strength of the Smooth button.
# gsFillWithQuads
Will fill the polygon hole with equally spaced quads based on the selection patterns:
1. One vert selected - will produce a best guess fill. Can incorrectly estimate the shape of the polygon hole.
2. Two verts selected - the best way to control the distribution of quads.
Selecting two corners of the hole will ensure the best result.
Selecting any other two verts will change the pattern which might be desirable for creative purposes.
# gsCombine
Will combine selected meshes into one cleanly. All the extraneous transform nodes will be grouped and hidden in the outliner. To view them, select "Ignore Hidden In Outliner".
# gsSeparate
Will separate selected meshes cleanly. All the extraneous transform nodes will be grouped and hidden in the outliner. To view them, select "Ignore Hidden In Outliner".
# gsDuplicate
Will duplicate selected faces from the object cleanly. All the extraneous transform nodes will be grouped and hidden in the outliner. To view them, select "Ignore Hidden In Outliner".
# gsExtract
Will extract selected faces from the object cleanly. All the extraneous transform nodes will be grouped and hidden in the outliner. To view them, select "Ignore Hidden In Outliner".
Normal Click - extract the faces but do not separate them from the object.
Shift + Click - extract the faces and separate them into a separate object.
<!-- Booleans -->
# gsBoolUnion
Will create a Boolean union from the selected objects. The Booleans will be editable post-operation.
Normal Click - Boolean objects are visible post-operation.
Shift + Click - Boolean objects are hidden post-operation.
Booleans are compatible with Instance+ objects.
# gsBoolDifference
Will create a Boolean difference from the selected objects. The Booleans will be editable post-operation.
Normal Click - Boolean objects are visible post-operation.
Shift + Click - Boolean objects are hidden post-operation.
Booleans are compatible with Instance+ objects.
# gsBoolIntersection
Will create a Boolean intersection from the selected objects. The Booleans will be editable post-operation.
Normal Click - Boolean objects are visible post-operation.
Shift + Click - Boolean objects are hidden post-operation.
Booleans are compatible with Instance+ objects.
# gsToggleBoolGroup
Toggle the viewport visibility of the Boolean objects.
# gsShowCutters
Show all the Boolean objects.
# gsSelectCutters
Select all the Boolean objects.
# gsDeleteCutter
Remove selected Boolean object from the Boolean operation.
# gsApplyBoolean
Apply all the Booleans to the selected mesh.
Click - construction history is not deleted.
Shift + Click - construction history is deleted.
<!-- Live Plane -->
# gsLivePlane
Create a Live Plane on the selected polygon. Live Plane will be aligned to the polygon and allows for precise placement of the meshes on it.
Live Plane can be edited post-creation using slider below and/or manipulating it using gizmo.
Clicking on Live Plane button again will remove the Live Plane.
# gsSwitchToCam
Switch to Live Plane aligned camera for precise object placement on the plane.
Clicking again will return back to the original camera.
# gsAlignToPlane
Align the selected object to the live plane (object X axis by default).
Hold RMB for additional Axes:
- X,Y,Z,-X,-Y,-Z - will align the object based on these axes.
# gsLivePlaneResolutionSlider
Controls the resolution of the live plane grid.
# gsLivePlaneSpacingField
Controls the spacing of the live plane grid.
<!-- Material Slots -->
# gsMaterialSlot0
Quick Material slot. Allows for quick assigning of the materials, storing custom materials (scene independent if saved as a preset) and creating MatCap materials.
Left Click - assign the material to the selected object(s).
Shift + LMB - Select the material from the current swatch. It can be then edited in the Attribute Editor.
Ctrl + LMB - Select all the polygons or objects that have the material assigned to them.
Shift + Ctrl + LMB - Select the meshes that have the material assigned to them.
Hold RMB - open marking menu:
- Select From Scene Materials - select a material from the current scene and add it to the Quick Material Slot.
- Create Maya Material - create a standard Maya material and add it to the Slot.
- Create MatCap Material - create a "material capture" shader and add it to the Slot. Will open a texture select window where appropriate MatCap texture must be selected.
- Save Preset - save the current Slot as preset material. Presets can be accessed from any scene later on.
- Clear Slot - remove the material from the slot. Will not delete the material from the scene.
- Manage Presets - open a preset management window where presets can be browsed, added to the slot and deleted.
- Global Shader checkbox - make the current slot a default material for all the new objects in the scene.
<!-- Randomize Window -->
# gsTranslationRandBox
Enables the translation randomization.
# gsTranslationLocalBox
Randomization will be in local space.
# gsTranslationXBox
Randomization will be on X axis (multiple axes are selectable).
# gsTranslationYBox
Randomization will be on Y axis (multiple axes are selectable).
# gsTranslationZBox
Randomization will be on Z axis (multiple axes are selectable).
# gsTranslateMulti
Translation randomization multiplier.
# gsTranslationRandRate
Translation randomization rate (preview). To apply, press Randomize button.
# gsRotationRandBox
Enables the rotation randomization.
# gsRotationLocalBox
Randomization will be in local space.
# gsRotationXBox
Randomization will be on X axis (multiple axes are selectable).
# gsRotationYBox
Randomization will be on Y axis (multiple axes are selectable).
# gsRotationZBox
Randomization will be on Z axis (multiple axes are selectable).
# gsRotationMulti
Rotation randomization multiplier.
# gsRotationRandRate
Rotation randomization rate (preview). To apply, press Randomize button.
# gsScaleRandBox
Enables the scale randomization.
# gsScaleXBox
Randomization will be on X axis (multiple axes are selectable).
# gsScaleYBox
Randomization will be on Y axis (multiple axes are selectable).
# gsScaleZBox
Randomization will be on Z axis (multiple axes are selectable).
# gsScaleMulti
Scale randomization multiplier.
# gsScaleRandRate
Scale randomization rate (preview). To apply, press Randomize button.
# gsRandomizeButton
Clicking this button will apply currently selected randomization pattern.
All the "Enable" toggles and slider values with other options will affect the randomization.
<!-- Array Control Window -->
# showOriginalArrayButton
Shows/hides the original object that can be edited to affect the arrayed objects.
# copiesSlider
Number of arrayed copies.
# calculateRotationLinearArrayButton
Whether to calculate the rotation of each individual object attached to a curve.
# stretchSlider
The amount the objects should stretch along the curve (0 - no stretch, 1 - all the way to the end of the curve).
# offsetSlider
Offsets the objects along the curve.
# startPoint
Offsets the start point of the array along the curve.
# stretchAlongCurve
The arrayed objects will stretch to the full length of the curve. Disables the Length slider.
# mergeVertsLinearDeformed
The arrayed objects will have their verts merged based on the Merge Tolerance.
# uniformDistributionLinearDeformed
The objects will be distributed based on their bounding box.
# offsetDeformedSlider
Offset arrayed objects along the curve.
# orientationSlider
Rotate the arrayed objects around the curve.
# twistSlider
Gradually changes the orientation of each arrayed object, not deforming the individual objects.
# twistDeformSlider
Twists the array shape. Deforms the individual objects.
# lengthScaleSlider
How far the arrayed objects will stretch along the curve.
# linearScaleYSlider
Gradually changes the Y scale of each individual object in the array.
# linearScaleZSlider
Gradually changes the Z scale of each individual component of the array.
# widthScaleSlider
Change the width of the arrayed objects.
# mergeDistanceSlider
Controls the merge distance used by the "Merge Verts" toggle.
# samplingAccuracySlider
How accurate should the objects follow the curve. Values above 2 can affect performance.
# calculateRotationRadialArray
Whether to calculate the rotation of each individual in a radial array.
# gsRadialArrayAxisXY
Radial array is aligned with XY plane.
# gsRadialArrayAxisYZ
Radial array is aligned with YZ plane.
# gsRadialArrayAxisZX
Radial array is aligned with ZX plane.
# linearRadiusSlider
Controls the radius of the radial array object.
# zOffsetSlider
Offsets the radial array in Z axis in a spiral pattern.
# angleSlider
Controls the final angle of the radial array object.
# radialArrayUniformDistribution
The objects will be distributed based on their bounding box.
# radialDeformedArrayMergeVerts
The arrayed objects will have their verts merged based on the Merge Tolerance.
# radialCurvatureSlider
The bend angle of the radial array object.
# lowBoundSlider
The low bound at which the bending of the radial array begins.
# highBoundSlider
The high bound at which the bending of the radial array ends.
# deformedRadiusSlider
The radius of the radial array object.
# xTwist
Twists the array shape. Will not distort each individual array object.
# yDeformedOffsetSlider
Offsets the arrayed objects on Y axis.
# zDeformedOffsetSlider
Offsets the arrayed objects on Z axis.
# scaleYSlider
Scale each individual array object on Y axis.
# scaleZSlider
Scale each individual array object on Z axis.
# patternOffsetSlider
Offsets the array pattern 1 object at a time. Will change the scale sliders behavior.
# radialDeformedMergeDistance
Controls the merge distance used by the "Merge Verts" toggle.
# patternSlider
Controls the pattern of the multi-arrayed object (when using multiple objects in array creation or using the + button).
1 - A A A A A...
2 - A B A B A...
3 - A B B A B B...
etc.
# patternRandomizeSlider
The seed used to randomize the multi-array pattern (when using multiple objects in array creation or using the + button).
0 - disable randomization.
# arrayUniformScale
Use uniform scale randomization (from X, Y, Z scale only X will be used).
# randMagnitudeSlider
The amount of transform randomization to apply to the arrayed object.
# randTXSlider
Randomize the position of the objects on X axis.
# randTYSlider
Randomize the position of the objects on Y axis.
# randTZSlider
Randomize the position of the objects on Z axis.
# randRXSlider
Randomize the rotation of the objects on X axis.
# randRYSlider
Randomize the rotation of the objects on Y axis.
# randRZSlider
Randomize the rotation of the objects on Z axis.
# randSXSlider
Randomize the scale of the objects on the X axis.
In Uniform Scale mode this slider acts as a X, Y, Z randomize slider.
# randSYSlider
Randomize the scale of the objects on the Y axis.
Disabled when "Uniform Scale" is enabled.
# randSZSlider
Randomize the scale of the objects on the Z axis.
Disabled when "Uniform Scale" is enabled.
# randSeedSlider
Randomization seed. Used to change the randomization pattern.
# ResetSliders
Resets all the sliders to their default range.
Useful when the sliders become hard to control when entering values manually to the fields.

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

View File

@@ -0,0 +1,34 @@
GS Toolbox installation
1. Copy gs_toolbox folder to {PathToDocuments}\Documents\Maya\{MayaVersion}\scripts\
Example of the final folder structure:
Documents\Maya\2019\scripts\gs_toolbox\icons
Documents\Maya\2019\scripts\gs_toolbox\presets
Documents\Maya\2019\scripts\gs_toolbox\gs_shaderball.obj
Documents\Maya\2019\scripts\gs_toolbox\gs_toolbox_doc.pdf
Documents\Maya\2019\scripts\gs_toolbox\gs_toolbox_init.mel
Documents\Maya\2019\scripts\gs_toolbox\gs_toolbox_proc.mel
Documents\Maya\2019\scripts\gs_toolbox\gs_toolbox_reset.mel
Documents\Maya\2019\scripts\gs_toolbox\gs_toolbox_startup.mel
Documents\Maya\2019\scripts\gs_toolbox\gs_toolbox_stop.mel
Documents\Maya\2019\scripts\gs_toolbox\main.mel
Documents\Maya\2019\scripts\gs_toolbox\readme.txt
2. Run Maya
3. In "Python" command line, run this command:
import gs_toolbox.init as tb_init;from imp import reload;reload(tb_init);tb_init.Init();
4. Look for GS tab on your Shelf
5. Click TB UI button to run the menu. Click again to hide the menu.
>> To reset to factory defaults click TB with refresh arrow button.
>> To stop all scripts and close the menu press TB DEL button.
>> You can use middle-mouse button drag to move the buttons to any tab.
>> All the hotkeys are available in Hotkey Editor > Custom Scripts > GS > GS_Toolbox

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,218 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import maya.cmds as cmds
# Define minimum window width and height
MIN_WINDOW_WIDTH = 300 # Increased from 200 to 300
MIN_WINDOW_HEIGHT = 300
# Function: Get and display UV sets
def refresh_uv_sets():
selection = cmds.ls(selection=True)
if not selection:
cmds.warning("Please select an object first.")
return
selected_object = selection[0]
uv_sets = cmds.polyUVSet(selected_object, query=True, allUVSets=True)
cmds.textScrollList('uvList', edit=True, removeAll=True)
for uv_set in uv_sets:
cmds.textScrollList('uvList', edit=True, append=uv_set)
# Function: Switch UV set
def switch_uv_set(*args):
selected_uv_set = cmds.textScrollList('uvList', query=True, selectItem=True)
if selected_uv_set:
selected_object = cmds.ls(selection=True)
if selected_object:
selected_object = selected_object[0]
# Switch current UV set
cmds.polyUVSet(selected_object, currentUVSet=True, uvSet=selected_uv_set[0])
print("Switched to UV set: {}".format(selected_uv_set[0]))
else:
cmds.warning("Please select an object.")
else:
cmds.warning("Please select a UV set.")
# Function: Delete selected UV set
def delete_selected_uv_set():
selected_uv_set = cmds.textScrollList('uvList', query=True, selectItem=True)
if selected_uv_set:
cmds.polyUVSet(delete=True, uvSet=selected_uv_set[0])
refresh_uv_sets()
# Function: Rename selected UV set
def rename_selected_uv_set(new_name):
selected_uv_set = cmds.textScrollList('uvList', query=True, selectItem=True)
if selected_uv_set:
if new_name:
cmds.polyUVSet(rename=True, newUVSet=new_name, uvSet=selected_uv_set[0])
refresh_uv_sets()
cmds.textFieldGrp('newNameField', edit=True, text='') # Clear input field content
else:
cmds.warning("Please enter a new name.")
else:
cmds.warning("Please select a UV set first.")
# Function: Create new UV set
def create_new_uv_set(new_name):
if new_name:
cmds.polyUVSet(create=True, uvSet=new_name)
refresh_uv_sets()
cmds.textFieldGrp('newNameField', edit=True, text='') # Clear input field content
else:
cmds.warning("Please enter a name for the new UV set.")
# Function: Set UV set 1 name
def set_uv_set1_name(*args):
selected_uv_set = cmds.textScrollList('uvList', query=True, selectItem=True)
if selected_uv_set:
cmds.textFieldGrp("uvSet1", edit=True, text=selected_uv_set[0])
else:
cmds.warning("Please select a UV set first.")
# Function: Set UV set 2 name
def set_uv_set2_name(*args):
selected_uv_set = cmds.textScrollList('uvList', query=True, selectItem=True)
if selected_uv_set:
cmds.textFieldGrp("uvSet2", edit=True, text=selected_uv_set[0])
else:
cmds.warning("Please select a UV set first.")
# Function: UV set swap
def UVsetSwap(*args):
UVname1 = cmds.textFieldGrp("uvSet1", query=True, text=True)
UVname2 = cmds.textFieldGrp("uvSet2", query=True, text=True)
cmds.polyUVSet(query=True, allUVSets=True)
cmds.polyUVSet(create=True, uvSet='TempUV')
cmds.polyUVSet(copy=True, nuv='TempUV', uvSet=UVname1)
cmds.polyUVSet(copy=True, nuv=UVname1, uvSet=UVname2)
cmds.polyUVSet(copy=True, nuv=UVname2, uvSet='TempUV')
cmds.polyUVSet(delete=True, uvSet='TempUV')
refresh_uv_sets() # Refresh list after execution
def UVsetReorder(*args):
UVname1 = cmds.textFieldGrp("uvSet1", query=True, text=True)
UVname2 = cmds.textFieldGrp("uvSet2", query=True, text=True)
print("Reorder object is " + UVname1 + " + " + UVname2)
cmds.polyUVSet(reorder=True, uvSet=UVname1, newUVSet=UVname2)
UVobj = cmds.ls(sl=True)
cmds.select(UVobj)
refresh_uv_sets() # Refresh list after execution
# Function: UV set transfer
def get_object_name(*args):
# Get currently selected object and fill its name in the text field
selected = cmds.ls(sl=True)
if selected:
cmds.textField('objectNameField', edit=True, text=selected[0])
else:
cmds.warning("No object selected.")
def set_uv(*args):
# Get source and target objects, perform UV transfer, and clean up history
source_object = cmds.textField('objectNameField', query=True, text=True)
target_object = cmds.ls(sl=True)
if not source_object or not target_object:
cmds.warning("Please ensure both source and target objects are selected.")
return
target_object = target_object[0]
sample_space_dict = {'World': 0, 'Local': 1, 'UV': 5, 'Component': 4}
sample_space = cmds.radioCollection('sampleSpaceRadio', query=True, select=True)
sample_space = cmds.radioButton(sample_space, query=True, label=True)
sample_space = sample_space_dict.get(sample_space, 0)
cmds.transferAttributes(source_object, target_object, transferPositions=0, transferNormals=0, transferUVs=2, transferColors=0, sampleSpace=sample_space, searchMethod=3)
cmds.delete(target_object, constructionHistory=True) # Clean up history
def on_window_resize(*args):
window_name = "UVSetEditor"
# Get current window size
current_width = cmds.window(window_name, query=True, width=True)
current_height = cmds.window(window_name, query=True, height=True)
# Check and limit window size
if current_width < MIN_WINDOW_WIDTH:
cmds.window(window_name, edit=True, width=MIN_WINDOW_WIDTH)
if current_height < MIN_WINDOW_HEIGHT:
cmds.window(window_name, edit=True, height=MIN_WINDOW_HEIGHT)
def show(*args):
window_name = "UVSetEditor"
# Check if window exists, if so, delete it
if cmds.window(window_name, exists=True):
cmds.deleteUI('UV Set Editor', window=True)
# Window
# Create a new window and set its title and initial size
window = cmds.window(window_name, title=" UV Set Editor", widthHeight=(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT), sizeable=True, tlb=True) # tlb=True
cmds.frameLayout(label='UV-Set')
cmds.columnLayout(adjustableColumn=True)
# Create a textScrollList control and set the selection change command
cmds.textScrollList('uvList', numberOfRows=8, allowMultiSelection=False, width=280, selectCommand=switch_uv_set)
cmds.textFieldGrp('newNameField', placeholderText=' Enter new name, then click Re to rename', width=280, columnAlign=[1, 'center'] ,columnWidth=[1,280])
cmds.rowLayout(numberOfColumns=4,
columnWidth4=(65, 65, 65, 65),
columnAttach4=('both', 'both', 'both', 'both'))
cmds.button( label='Get', height=32, command=lambda x: refresh_uv_sets(),backgroundColor=(0.53, 0.81, 0.98))
cmds.button( label='Del', height=32, command=lambda x: delete_selected_uv_set())
cmds.button( label='New', height=32, command=lambda x: create_new_uv_set(cmds.textFieldGrp('newNameField', query=True, text=True)))
cmds.button( label='Re', height=32, command=lambda x: rename_selected_uv_set(cmds.textFieldGrp('newNameField', query=True, text=True)))
cmds.setParent( '..' )
cmds.setParent('..') # End current form layout
cmds.frameLayout(label='UV-Swap')
cmds.columnLayout(adjustableColumn=True, width=280)
cmds.text(l='Enter UV set names in "uv1" and "uv2"', h=15)
cmds.text(l=' UV swap or reorder swap. ', h=15)
cmds.text(l='', h=5)
cmds.rowLayout(numberOfColumns=3, columnWidth3=(65, 65, 130), columnAttach3=('both', 'both', 'both'))
cmds.button(label='Get', height=25, command=set_uv_set1_name, backgroundColor=(0.53, 0.81, 0.98))
cmds.textFieldGrp("uvSet1", placeholderText='uv1', editable=True, width=200)
cmds.setParent('..')
cmds.rowLayout(numberOfColumns=3, columnWidth3=(65, 65, 130), columnAttach3=('both', 'both', 'both'))
cmds.button(label='Get', height=25, command=set_uv_set2_name, backgroundColor=(0.53, 0.81, 0.98))
cmds.textFieldGrp("uvSet2", placeholderText='uv2', editable=True, width=200)
cmds.setParent('..')
cmds.rowColumnLayout(numberOfColumns=2, columnWidth=[(1, 135), (2, 135)])
cmds.button(label='UV Swap', command=UVsetSwap, backgroundColor=(0.53, 0.81, 0.98))
cmds.button(label='Reorder Swap', command=UVsetReorder, backgroundColor=(0.53, 0.81, 0.98))
UVname1 = cmds.textFieldGrp("uvSet1", query=True, text=True)
UVname2 = cmds.textFieldGrp("uvSet2", query=True, text=True)
print("Now we have UVset = {}, {}".format(UVname1, UVname2))
cmds.setParent('..') # End current form layout
# Separator
cmds.separator(height=20, style='in')
# Create a column layout, all child elements will be vertically arranged
cmds.frameLayout(label='UV-Transfer')
cmds.columnLayout(adjustableColumn=True, width=230, height=130)
cmds.rowLayout(numberOfColumns=3, columnWidth3=(50, 100, 50))
cmds.button(label='Get', command=get_object_name, backgroundColor=(0.53, 0.81, 0.98), width=45) # Create a button that calls get_object_name function when clicked
cmds.textField('objectNameField', enable=False, width=120) # Create a text field to display the name of the selected object
cmds.button(label='Set', command=set_uv, backgroundColor=(0.53, 0.81, 0.98), width=45) # Create a button that calls set_uv function when clicked
cmds.setParent('..') # End current form layout
# cmds.frameLayout(label='Sample Space')
cmds.text(l='Sample Space:', h=20, align='left')
form = cmds.formLayout()
cmds.radioCollection('sampleSpaceRadio') # Create a radio button group
rb1 = cmds.radioButton(label='World', select=True) # Create a radio button
rb2 = cmds.radioButton(label='Local')
rb3 = cmds.radioButton(label='UV')
rb4 = cmds.radioButton(label='Component')
# Set form layout parameters to keep radio buttons horizontally aligned and centered when window size changes
cmds.formLayout(form, edit=True, attachForm=[(rb1, 'left', 10), (rb4, 'right', 10)], attachControl=[(rb2, 'left', 5, rb1), (rb3, 'left', 5, rb2), (rb4, 'left', 5, rb3)])
cmds.setParent('..') # End current form layout
cmds.scriptJob(event=["idle", on_window_resize], parent=window) # Listen for window resize events
cmds.showWindow(window) # Show window
if __name__ == "__main__":
show()

View File

@@ -0,0 +1,60 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''
# Author: Wanshihui
# Date: 2023-11-27
# Version: 1.0
# Description: Copy skinCluster from source models to target models
'''
import maya.cmds as cmds
def copy_skinCluster_to_target():
# Get all models that start with "CSW_"
source_models = cmds.ls("CSW_*", type="transform")
if not source_models:
cmds.warning("No model starting with 'CSW_' was found in the scene.")
return
for src in source_models:
# Get all models that start with "CSW_"
suffix = src[4:]
# Find the target model with the corresponding suffix.
target = suffix
if not cmds.objExists(target):
cmds.warning(f"No corresponding target model found: {target}")
continue
# Find the skinCluster of the src model
skin_clusters = cmds.ls(cmds.listHistory(src), type='skinCluster')
if not skin_clusters:
cmds.warning(f"{src} No skinCluster is bind.")
continue
skin_cluster = skin_clusters[0]
# Copy skinCluster to the target model
try:
# First, delete any existing skinClusters in the target model (if have).
target_skin_clusters = cmds.ls(cmds.listHistory(target), type='skinCluster')
if target_skin_clusters:
for tsc in target_skin_clusters:
cmds.delete(tsc)
# Perform replication of skinCluster weights
# Bind a new skinCluster
new_skin_cluster = cmds.skinCluster(cmds.skinCluster(skin_cluster, q=True, inf=True), target, toSelectedBones=True)[0]
# copy skin weights
cmds.copySkinWeights(ss=skin_cluster, ds=new_skin_cluster, noMirror=True, surfaceAssociation='closestPoint', influenceAssociation=['name', 'closestJoint'])
print(f"From skinCluster to '{src}'copy'{target}' successful!")
except Exception as e:
cmds.warning(f"From'{src}'copy skinCluster to'{target}'failed: {e}")
print("ALL skinCluster copy successful!")
# If run this script directly
if __name__ == "__main__":
copy_skinCluster_to_target()

View File

@@ -1,147 +0,0 @@
# Skin API Module
强大的 Maya 蒙皮权重管理工具,支持权重的导出、导入和蒙皮解绑功能。
## 📁 文件结构
```
skin_api/
├── __init__.py # 模块初始化
├── ui.py # UI 函数(导出、导入、解绑)
├── apiVtxAttribs.py # 核心 API 类
├── Skinning.py # 蒙皮操作函数
├── Utils.py # 工具函数
└── README.md # 本文档
```
## 🚀 使用方法
### 从工具架启动
1. 打开 Maya
2. 切换到 **Nexus_Rigging** 工具架
3. 使用对应的按钮:
- **Export Weights** - 导出蒙皮权重
- **Import Weights** - 导入蒙皮权重
- **Unbind Skin** - 解绑蒙皮
### 从 Python 调用
#### 导出权重
```python
from rigging_tools.skin_api import WeightExport
WeightExport()
```
#### 导入权重
```python
from rigging_tools.skin_api import WeightImport
WeightImport()
```
#### 解绑蒙皮
```python
from rigging_tools.skin_api import UnbindSkin
UnbindSkin()
```
#### 使用核心 API
```python
from rigging_tools.skin_api import ApiVtxAttribs
# 创建 API 实例
api = ApiVtxAttribs()
# 导出权重
api.exportSkinWeights(selected=True, saveJointInfo=True)
# 导入权重
api.importSkinWeights(selected=False, stripJointNamespaces=False, addNewToHierarchy=True)
```
## ✨ 主要功能
- **权重导出** - 导出选中或所有蒙皮物体的权重数据
- **权重导入** - 导入权重到选中或匹配的物体
- **蒙皮解绑** - 快速解绑选中物体的蒙皮
- **关节信息保存** - 可选保存关节方向、世界变换和父级信息
- **智能匹配** - 基于名称和顶点数量自动匹配物体
- **命名空间处理** - 支持剥离关节命名空间
## 🔧 版本兼容性
### 支持的 Maya 版本
- **所有 Maya 版本** - 从 Maya 2016 到 Maya 2025+
- **Maya 2025** - 完全兼容,修复了 PyMEL 相关问题
### API 兼容性
模块采用双重 API 支持策略:
- **PyMEL** - 优先使用 PyMEL如果可用
- **Maya Commands** - 自动降级到 `maya.cmds`(如果 PyMEL 不可用)
- **Maya API** - 使用 `maya.OpenMaya``maya.OpenMayaAnim` 进行高性能操作
### 兼容性特性
1. **自动 API 检测** - 运行时检测可用的 API
2. **优雅降级** - PyMEL 不可用时自动使用 cmds
3. **相对导入** - 支持作为包导入或独立模块使用
4. **异常处理** - 完善的错误处理和用户提示
5. **空值安全** - 处理节点无父节点等边界情况
6. **Maya 2025 优化** - 修复 PyMEL 在新版本中的兼容性问题
## 📝 文件格式
权重文件使用 `.skinWeights` 格式(基于 Python pickle
- 包含顶点权重数据
- 包含影响对象(关节)信息
- 可选包含关节变换信息
- 支持多物体批量导出
## 💡 使用技巧
### 导出权重
1. 选择需要导出的蒙皮物体
2. 运行 `WeightExport()`
3. 选择保存位置和文件名
4. 建议启用 `saveJointInfo` 以保存完整的关节信息
### 导入权重
1. **选中物体导入** - 选择目标物体后导入(仅导入到选中物体)
2. **自动匹配导入** - 不选择物体导入(自动匹配场景中的所有物体)
3. 确保物体名称和顶点数量匹配
4. 如果关节缺失,可启用 `addNewToHierarchy` 自动创建
### 解绑蒙皮
1. 选择需要解绑的物体
2. 运行 `UnbindSkin()`
3. 确认对话框后执行解绑
4. 支持批量解绑多个物体
## ⚠️ 注意事项
- 导入权重时,目标物体的顶点数量必须与导出时一致
- 物体名称需要匹配(支持短名称匹配)
- 建议在导入前备份场景
- 大量物体操作时会显示进度条
- 权重文件使用 pickle 格式,不同 Python 版本间可能存在兼容性问题
### Maya 2025 特别说明
- 已修复 PyMEL 在处理无父节点骨骼时的 `'NoneType' object has no attribute 'name'` 错误
- 增强了所有 PyMEL 对象的空值检查
- 建议使用 `saveJointInfo=True` 导出完整的骨骼信息
## 🐛 故障排除
### 导入失败
- 检查物体名称是否匹配
- 检查顶点数量是否一致
- 确认权重文件路径正确
- 查看 Maya 脚本编辑器的详细错误信息
### 关节缺失
- 启用 `addNewToHierarchy` 参数
- 确保导出时使用了 `saveJointInfo=True`
- 手动创建缺失的关节
### PyMEL 相关问题
- 模块会自动降级到 cmds无需担心
- 如果需要强制使用 cmds可以在导入前设置环境变量

View File

@@ -1,3 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Site : Virtuos Games
# @Author : ZHou Shuhua
try:
import pymel.core as pm
except ImportError:
@@ -112,8 +118,8 @@ def getSkinClusterInfo(objectName, saveJointInfo=False):
def getSkinJointInformation(influences):
"""
获取骨骼信息(父节点、矩阵、旋转、关节方向)
兼容 PyMEL cmds,处理无父节点的情况
Retrieve skeletal information (parent node, matrix, rotation, joint orientation)
Compatible with PyMEL and cmds, handling cases without parent nodes.
"""
jointInformation = {}
@@ -122,7 +128,7 @@ def getSkinJointInformation(influences):
try:
if pm:
infNode = pm.PyNode(inf)
# 安全获取父节点,避免 None.name() 错误
# Safely retrieve the parent node and avoid the None.name() error.
parent = infNode.getParent()
jointInfo["parent"] = str(parent.name()) if parent else ""
jointInfo["matrix"] = infNode.getMatrix(worldSpace=True)
@@ -130,7 +136,7 @@ def getSkinJointInformation(influences):
jointInfo["jointOrient"] = infNode.getAttr("jointOrient")
jointInformation[str(infNode)] = copy.deepcopy(jointInfo)
else:
# cmds 版本
# cmds verison
infName = str(inf)
parents = cmds.listRelatives(infName, parent=True)
jointInfo["parent"] = parents[0] if parents else ""
@@ -140,7 +146,7 @@ def getSkinJointInformation(influences):
jointInformation[infName] = copy.deepcopy(jointInfo)
except Exception as e:
print(f"Warning: Failed to get joint information for {inf}: {e}")
# 使用默认值
# Use default values
jointInfo["parent"] = ""
jointInfo["matrix"] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
jointInfo["rotation"] = [0, 0, 0]
@@ -368,11 +374,11 @@ def buildSkinWeightsDict(objectList, showLoadingBar=True, saveJointInfo=False):
for object in objectList:
try:
if pm:
# 安全转换为字符串,处理可能的 None 或无效对象
# Safely convert to string, handling possible None or invalid objects.
obj_node = pm.PyNode(object) if not isinstance(object, pm.PyNode) else object
objectAsString = str(obj_node.name()) if obj_node else str(object)
else:
# cmds 版本 - object 已经是字符串
# cmds version - object is already a string
objectAsString = str(object)
except Exception as e:
print(f"Warning: Failed to process object {object}: {e}")
@@ -416,7 +422,7 @@ def transferSkinWeights(transferNodes=None, showLoadingBar=True):
if len(transferNodes):
sourceObj = transferNodes[0]
# 安全获取名称
# Get name
try:
sourceName = str(sourceObj.name()) if hasattr(sourceObj, 'name') else str(sourceObj)
except:
@@ -436,7 +442,7 @@ def transferSkinWeights(transferNodes=None, showLoadingBar=True):
# deep copy because: Mutable datatypes
sourceWeightDictCopy = copy.deepcopy(sourceWeightDict)
# 安全获取名称
# Get name
try:
targetName = str(tgtObject.name()) if hasattr(tgtObject, 'name') else str(tgtObject)
except:
@@ -526,7 +532,7 @@ def skinClusterBuilder(objName, weightDict, deleteHist=True, stripJointNamespace
joint.setMatrix(jointInfo.get("matrix"), worldSpace=True)
pm.select(cl=True)
else:
# cmds 版本
# cmds version
cmds.select(clear=True)
joint = cmds.joint(position=(0, 0, 0), name=jointName)
# putting joint in the hierarchy and setting matrix
@@ -557,7 +563,7 @@ def skinClusterBuilder(objName, weightDict, deleteHist=True, stripJointNamespace
pm.setAttr('%s.normalizeWeights' % clusterNode, 1)
clusterNodeName = str(clusterNode)
else:
# cmds 版本
# cmds verison
clusterNode = cmds.skinCluster(clusterJoints, objName, tsb=True, mi=clusterMaxInf, omi=True)[0]
# turn of normalization to nuke weights to 0, this is to get a true 1->1 application of old weights
cmds.setAttr('%s.normalizeWeights' % clusterNode, 0)

View File

@@ -1,3 +1,9 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Site : Virtuos Games
# @Author : ZHou Shuhua Cai Jianbo
try:
import pymel.core as pm
except ImportError:

View File

@@ -1,16 +1,19 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Site : Virtuos Games
# @Author : ZHou Shuhua
"""
Skin API Module
提供蒙皮权重导出、导入和管理功能
支持 Maya 所有版本,兼容 pymel cmds
Provides skin weight export, import, and management functions
Supports all Maya versions with compatibility for both pymel and cmds
"""
# 导出主要的 UI 函数
# Export main UI functions
from .ui import WeightExport, WeightImport, UnbindSkin
# 导出核心类
# Export core classes
from .apiVtxAttribs import ApiVtxAttribs
__all__ = [

View File

@@ -1,13 +1,16 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# @Site : Virtuos Games
# @Author : ZHou Shuhua
"""
Skin API UI Functions
提供权重导出、导入和解绑的用户界面函数
支持 Maya 所有版本,兼容 pymel cmds
Provides UI functions for weight export, import, and skin unbind
Supports all Maya versions with pymel and cmds compatibility
"""
# 尝试导入 pymel如果不可用则使用 cmds
# Try to import pymel, fall back to cmds if unavailable
try:
import pymel.core as pm
except ImportError:
@@ -27,7 +30,7 @@ def WeightExport():
Export skin weights for selected objects
"""
try:
# 检查是否有选中物体
# Check if there are selected objects
if pm:
selectedNodes = pm.ls(sl=True)
if not selectedNodes:
@@ -73,7 +76,7 @@ def WeightImport():
Import skin weights to selected or matching objects in scene
"""
try:
# 检查是否有选中物体
# Check if there are selected objects
if pm:
selectedNodes = pm.ls(sl=True)
else:
@@ -82,7 +85,7 @@ def WeightImport():
print(f"Import mode: {'selected objects' if useSelection else 'all matching objects'}")
# 使用 ApiVtxAttribs 类导入权重
# Use ApiVtxAttribs class to import weights
api = apiVtxAttribs.ApiVtxAttribs()
msg = api.importSkinWeights(selected=useSelection, stripJointNamespaces=False, addNewToHierarchy=True)
@@ -112,7 +115,7 @@ def WeightImport():
else:
cmds.warning(msg)
else:
# 成功导入
# Import successful
if pm:
pm.confirmDialog(
title="Import Complete",
@@ -140,11 +143,10 @@ def WeightImport():
def UnbindSkin():
"""
解绑选中物体的蒙皮
Unbind skin from selected objects
"""
try:
# 获取选中的物体
# Check selected objects
if pm:
selectedNodes = pm.ls(sl=True)
if not selectedNodes:
@@ -156,7 +158,7 @@ def UnbindSkin():
cmds.warning("Please select at least one object")
return
# 确认对话框
# Confirmation dialog
if pm:
result = pm.confirmDialog(
title="Unbind Skin",
@@ -180,7 +182,7 @@ def UnbindSkin():
print("Unbind cancelled")
return
# 使用 MEL 命令解绑蒙皮
# Use MEL command to unbind skin
mel.eval('doDetachSkin "2" { "1","1" };')
print("Skin unbound successfully!")

View File

@@ -2,199 +2,562 @@
# -*- coding: utf-8 -*-
"""
Nexus Maya 2023 - User Setup Script
Automatically executed when Maya starts
Nexus User Setup Script
Automatically sets up the Nexus plugin system in Maya upon startup.
Features:
- Auto-loads scripts, plugins, and shelves
- Sets up icon paths for UI elements
- Manages command port for external connections
- Clean exit with proper resource cleanup
- Maya 2018+ compatible
"""
import maya.cmds as cmds
import maya.mel as mel
import maya.utils
import atexit
import os
import sys
import re
# Silently try to open default commandPort to avoid startup error if it's already in use
try:
mel.eval('catchQuiet("commandPort -securityWarning -name \\"commandportDefault\\";");')
except Exception:
pass
# =============================================================================
# Configuration
# =============================================================================
# Shelves to load
SHELF_NAMES = ["Nexus_Modeling", "Nexus_Rigging", "Nexus_Animation"]
def load_nexus_shelves():
"""Load all Nexus shelves (force refresh)"""
# Tool packages configuration
TOOL_CONFIG = {
'scripts': [
{'name': 'animation_tools', 'path': 'animation_tools'},
{'name': 'modeling_tools', 'path': 'modeling_tools'},
{'name': 'rigging_tools', 'path': 'rigging_tools'},
],
'plugins': [
{'name': 'gs_curvetools', 'path': 'modeling_tools/gs_curvetools/plugins/2025'},
{'name': 'ngskintools2', 'path': 'rigging_tools/ngskintools2/plug-ins/2025'},
{'name': 'mgpicker_plugin', 'path': 'animation_tools/mgpicker/MGPicker_Program/Plug-ins'},
{'name': 'mgpicker_help', 'path': 'animation_tools/mgpicker/MGPicker_Help'},
],
'icons': [
{'name': 'gs_curvetools', 'path': 'modeling_tools/gs_curvetools/icons'},
{'name': 'gs_toolbox', 'path': 'modeling_tools/gs_toolbox/icons'},
{'name': 'ngskintools2', 'path': 'rigging_tools/ngskintools2/ui/images'},
{'name': 'mgpicker', 'path': 'animation_tools/mgpicker'},
{'name': 'atools', 'path': 'animation_tools/atools/img'},
{'name': 'dwpicker', 'path': 'animation_tools/dwpicker/icons'},
{'name': 'studiolibrary', 'path': 'rigging_tools/studiolibrary/'}
],
}
# Maya version compatibility check
MAYA_MIN_VERSION = 2018
# Debug control: set environment variable TOOL_DEBUG=1 for verbose logs
TOOL_DEBUG = os.environ.get('TOOL_DEBUG', '0') == '1'
# Global state - capture __file__ at module level before executeDeferred
try:
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
except NameError:
_SCRIPT_DIR = None
_ICONS_PATHS = {}
_LOADED_PLUGINS = {}
_COMMAND_PORT_OPENED = False
_ADDED_SCRIPT_PATHS = []
# =============================================================================
# Utility Functions
# =============================================================================
def _tool_log(msg):
"""Print debug message if TOOL_DEBUG is enabled"""
if TOOL_DEBUG:
print(msg)
def _tool_print(msg):
"""Print info message"""
print(msg)
def _get_script_dir():
"""Get the directory containing this script (with fallback)"""
global _SCRIPT_DIR
if _SCRIPT_DIR:
return _SCRIPT_DIR
# Try multiple methods to get script directory
try:
shelf_paths = os.environ.get('MAYA_SHELF_PATH', '')
if not shelf_paths:
print("[Nexus] MAYA_SHELF_PATH not set, trying alternative method...")
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
except NameError:
try:
import inspect
_SCRIPT_DIR = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
except Exception:
# Last resort: use current working directory's scripts folder
_SCRIPT_DIR = os.getcwd()
return _SCRIPT_DIR
def _norm_path(path):
"""Normalize path with forward slashes"""
return os.path.normpath(path).replace('\\', '/')
def _resolve_relative_path(path, base_dir):
"""Resolve relative path against base directory"""
if os.path.isabs(path):
return path
return os.path.normpath(os.path.join(base_dir, path))
def _safe_mel_eval(cmd):
"""Execute MEL command safely, return None on error"""
try:
return mel.eval(cmd)
except Exception:
return None
def _check_maya_version():
"""Check if Maya version meets minimum requirements"""
try:
maya_version = int(cmds.about(version=True).split()[0])
if maya_version < MAYA_MIN_VERSION:
_tool_print(f"[Tool] Warning: Maya {maya_version} detected. Minimum supported version is {MAYA_MIN_VERSION}")
return False
_tool_log(f"[Tool] Maya version: {maya_version}")
return True
except Exception as e:
_tool_print(f"[Tool] Warning: Could not determine Maya version: {e}")
return True # Continue anyway
def _find_file_in_paths(filename, search_paths):
"""Find file in a list of search paths"""
for p in search_paths:
if not p:
continue
candidate = os.path.join(p, filename)
if os.path.exists(candidate):
return candidate
return None
def _verify_shelf_icon_references(search_paths):
"""Verify that shelf icon references can be resolved"""
if not TOOL_DEBUG:
return # Only verify in debug mode
try:
for shelf_name in SHELF_NAMES:
shelf_file = os.path.join(search_paths[-1] if search_paths else '', f'shelf_{shelf_name}.mel').replace('\\', '/')
if not os.path.exists(shelf_file):
shelf_file = os.path.join(os.path.dirname(_get_script_dir()), 'shelves', f'shelf_{shelf_name}.mel').replace('\\', '/')
if not os.path.exists(shelf_file):
continue
try:
script_dir = os.path.dirname(os.path.abspath(__file__))
shelf_paths = os.path.join(os.path.dirname(script_dir), "shelves")
with open(shelf_file, 'r', encoding='utf-8') as fh:
data = fh.read()
except:
print("[Nexus] Could not determine shelf path, skipping shelf load")
continue
imgs = re.findall(r'-image\s+"([^"]+)"', data)
for img in imgs:
found = any(os.path.exists(os.path.join(p, img).replace('\\', '/')) for p in search_paths)
if not found:
_tool_log(f"[Tool] Warning: Icon '{img}' not found for shelf '{shelf_name}'")
except Exception as e:
_tool_log(f"[Tool] Icon verification error: {e}")
def _remove_shelf_config(shelf_name):
"""Remove shelf config from Maya prefs to force reload"""
try:
maya_version = cmds.about(version=True).split()[0]
maya_app_dir = os.environ.get('MAYA_APP_DIR', '')
if maya_app_dir:
shelf_config = os.path.join(maya_app_dir, maya_version, "prefs", "shelves", f"shelf_{shelf_name}.mel")
if os.path.exists(shelf_config):
os.remove(shelf_config)
_tool_log(f"[Tool] Removed cached shelf config: {shelf_name}")
except Exception as e:
_tool_log(f"[Tool] Could not remove shelf config: {e}")
# =============================================================================
# Setup Functions
# =============================================================================
def setup_icon_paths():
"""Configure icon paths in XBMLANGPATH environment variable"""
try:
script_dir = _get_script_dir()
icon_configs = TOOL_CONFIG.get('icons', [])
if not icon_configs:
_tool_log("[Tool] No icons configured")
return
# Build icon paths dictionary
icon_paths = {}
for cfg in icon_configs:
name = cfg.get('name', '')
path = cfg.get('path', '')
if not (name and path):
continue
full_path = _norm_path(_resolve_relative_path(path, script_dir))
if os.path.exists(full_path):
icon_paths[name] = full_path
else:
_tool_print(f"[Tool] Warning: Icon path not found: {full_path}")
# Check if already configured
global _ICONS_PATHS
if _ICONS_PATHS and _ICONS_PATHS == icon_paths:
_tool_log("[Tool] Icon paths already configured")
return
_ICONS_PATHS = icon_paths
# Update XBMLANGPATH
xbmlangpath = os.environ.get('XBMLANGPATH', '')
paths = [_norm_path(p.strip()) for p in xbmlangpath.split(os.pathsep) if p.strip()]
# Add icon paths to front
for icon_path in icon_paths.values():
if icon_path not in paths:
paths.insert(0, icon_path)
# Set environment variable
new_xbmlangpath = os.pathsep.join(paths)
os.environ['XBMLANGPATH'] = new_xbmlangpath
# Update MEL environment with proper escaping
try:
# Escape backslashes and quotes for MEL
safe_path = new_xbmlangpath.replace('\\', '/').replace('"', '\\"')
mel.eval(f'putenv "XBMLANGPATH" "{safe_path}";')
except Exception:
pass
_tool_log(f"[Tool] ✓ Icon paths configured: {len(icon_paths)} package(s)")
_verify_shelf_icon_references(paths)
except Exception as e:
_tool_print(f"[Tool] Error setting up icon paths: {e}")
def load_tool_shelves():
"""Load Nexus shelves into Maya"""
try:
# Determine shelf paths
shelf_paths = os.environ.get('MAYA_SHELF_PATH', '')
if not shelf_paths:
script_dir = _get_script_dir()
shelf_paths = os.path.join(os.path.dirname(script_dir), "shelves")
if not os.path.exists(shelf_paths):
_tool_print("[Tool] Shelf directory not found, skipping")
return
path_separator = ';' if os.name == 'nt' else ':'
shelf_path_list = shelf_paths.split(path_separator)
shelf_path_list = [p.strip() for p in shelf_paths.split(os.pathsep) if p.strip()]
# Load each shelf
loaded_count = 0
for shelf_name in SHELF_NAMES:
# Find shelf file
shelf_file_found = None
for shelf_path in shelf_path_list:
shelf_path = shelf_path.strip()
if not shelf_path:
continue
shelf_file = os.path.join(shelf_path, f"shelf_{shelf_name}.mel")
shelf_file = shelf_file.replace("\\", "/")
shelf_file = _norm_path(os.path.join(shelf_path, f"shelf_{shelf_name}.mel"))
if os.path.exists(shelf_file):
shelf_file_found = shelf_file
print(f"[Nexus] Found shelf file: {shelf_file}")
_tool_log(f"[Tool] Found shelf: {shelf_file}")
break
if not shelf_file_found:
print(f"[Nexus] Could not find shelf_{shelf_name}.mel")
_tool_print(f"[Tool] Shelf not found: shelf_{shelf_name}.mel")
continue
# Delete old shelf if exists
# Remove existing shelf
if cmds.shelfLayout(shelf_name, exists=True):
print(f"[Nexus] Deleting old shelf: {shelf_name}")
try:
cmds.deleteUI(shelf_name, layout=True)
_tool_log(f"[Tool] Removed existing shelf: {shelf_name}")
except Exception as e:
print(f"[Nexus] Warning: Could not delete old shelf: {e}")
# Load shelf using proper MEL method
print(f"[Nexus] Loading shelf: {shelf_name}")
_tool_log(f"[Tool] Could not remove shelf: {e}")
# Remove cached config
_remove_shelf_config(shelf_name)
try:
# Disable auto-save
mel.eval('optionVar -intValue "saveLastLoadedShelf" 0;')
# Disable shelf saving
_safe_mel_eval('optionVar -intValue "saveLastLoadedShelf" 0;')
# Create shelf layout
mel.eval(f'''
global string $gShelfTopLevel;
if (`shelfLayout -exists {shelf_name}`) {{
deleteUI -layout {shelf_name};
}}
setParent $gShelfTopLevel;
shelfLayout -cellWidth 35 -cellHeight 34 {shelf_name};
''')
print(f"[Nexus] ✓ Created shelf layout: {shelf_name}")
# Create shelf layout using Python API instead of MEL
try:
# Get shelf parent using safe MEL evaluation
shelf_parent = mel.eval('global string $gShelfTopLevel; $temp = $gShelfTopLevel;')
cmds.setParent(shelf_parent)
cmds.shelfLayout(shelf_name, cellWidth=35, cellHeight=34)
except Exception as e:
_tool_log(f"[Tool] Error creating shelf layout: {e}")
raise
# Set parent and execute shelf script
mel.eval(f'setParent {shelf_name};')
mel.eval(f'source "{shelf_file_found}";')
if not cmds.shelfLayout(shelf_name, exists=True):
raise RuntimeError(f"Failed to create shelf layout: {shelf_name}")
# Load shelf content with safe path handling
_safe_mel_eval(f'setParent {shelf_name};')
# Use forward slashes for cross-platform compatibility
safe_shelf_path = shelf_file_found.replace('\\', '/')
mel.eval(f'source "{safe_shelf_path}";')
mel.eval(f'shelf_{shelf_name}();')
print(f"[Nexus] ✓ Executed shelf script: shelf_{shelf_name}()")
# Verify shelf
if cmds.shelfLayout(shelf_name, exists=True):
buttons = cmds.shelfLayout(shelf_name, query=True, childArray=True) or []
if buttons:
print(f"[Nexus] ✓ Shelf loaded with {len(buttons)} button(s): {shelf_name}")
else:
print(f"[Nexus] ⚠ Shelf created but no buttons: {shelf_name}")
# Remove auto-saved config
try:
maya_version = cmds.about(version=True).split()[0]
maya_app_dir = os.environ.get('MAYA_APP_DIR', '')
if maya_app_dir:
shelf_config = os.path.join(maya_app_dir, maya_version, "prefs", "shelves", f"shelf_{shelf_name}.mel")
if os.path.exists(shelf_config):
os.remove(shelf_config)
print(f"[Nexus] ✓ Removed auto-saved config: {shelf_name}")
except Exception as e:
print(f"[Nexus] Warning: {e}")
else:
print(f"[Nexus] ✗ Shelf layout not created: {shelf_name}")
# Verify loaded
buttons = cmds.shelfLayout(shelf_name, query=True, childArray=True) or []
_tool_print(f"[Tool] ✓ Shelf loaded: {shelf_name} ({len(buttons)} buttons)")
loaded_count += 1
# Remove config again to prevent saving
_remove_shelf_config(shelf_name)
except Exception as e:
print(f"[Nexus] Error loading shelf {shelf_name}: {e}")
import traceback
traceback.print_exc()
continue
_tool_print(f"[Tool] Error loading shelf {shelf_name}: {e}")
_tool_log(f"[Tool] ✓ Shelves: {loaded_count}/{len(SHELF_NAMES)} loaded")
except Exception as e:
print(f"[Nexus] Error in load_nexus_shelves: {e}")
import traceback
traceback.print_exc()
_tool_print(f"[Tool] Error loading shelves: {e}")
def load_nexus_plugins():
def load_tool_plugins():
"""Load Nexus plugins"""
try:
plugin_paths = os.environ.get('MAYA_PLUG_IN_PATH', '')
if not plugin_paths:
print("[Nexus] MAYA_PLUG_IN_PATH not set")
script_dir = _get_script_dir()
plugin_configs = TOOL_CONFIG.get('plugins', [])
if not plugin_configs:
_tool_log("[Tool] No plugins configured")
return
path_separator = ';' if os.name == 'nt' else ':'
plugin_path_list = plugin_paths.split(path_separator)
plugins_to_load = ["nexus_plugin.py"]
for plugin_name in plugins_to_load:
plugin_found = False
for plugin_path in plugin_path_list:
plugin_path = plugin_path.strip()
if not plugin_path:
continue
plugin_file = os.path.join(plugin_path, plugin_name)
if os.path.exists(plugin_file):
plugin_found = True
break
if not plugin_found:
print(f"[Nexus] Plugin not found: {plugin_name}")
loaded_count = 0
for cfg in plugin_configs:
plugin_name = cfg.get('name', '')
env_var = cfg.get('env_var', 'MAYA_PLUG_IN_PATH')
if not plugin_name:
continue
_tool_log(f"[Tool] Processing plugin: {plugin_name}")
# Search for plugin in environment paths
plugin_path = _find_file_in_paths(plugin_name, os.environ.get(env_var, '').split(os.pathsep))
# Fallback to script directory and parent plug-ins folder
if not plugin_path:
search_dirs = [
script_dir,
os.path.join(os.path.dirname(script_dir), 'plug-ins')
]
plugin_path = _find_file_in_paths(plugin_name, search_dirs)
if not plugin_path:
_tool_print(f"[Tool] Plugin not found: {plugin_name}")
continue
# Add plugin directory to environment
plugin_dir = _norm_path(os.path.dirname(plugin_path))
current_paths = [p for p in os.environ.get(env_var, '').split(os.pathsep) if p]
if plugin_dir not in current_paths:
os.environ[env_var] = os.pathsep.join([plugin_dir] + current_paths)
# Get plugin basename
plugin_basename = os.path.splitext(os.path.basename(plugin_path))[0]
# Check if already loaded
try:
already_loaded = bool(cmds.pluginInfo(plugin_basename, query=True, loaded=True))
except Exception:
already_loaded = False
if already_loaded:
_tool_log(f"[Tool] Plugin already loaded: {plugin_basename}")
_LOADED_PLUGINS[plugin_basename] = plugin_path
continue
# Load plugin
try:
if not cmds.pluginInfo(plugin_name, query=True, loaded=True):
cmds.loadPlugin(plugin_name)
print(f"[Nexus] ✓ Plugin loaded: {plugin_name}")
else:
print(f"[Nexus] Plugin already loaded: {plugin_name}")
except Exception as e:
print(f"[Nexus] Error loading plugin {plugin_name}: {e}")
cmds.loadPlugin(plugin_path, quiet=True)
_tool_print(f"[Tool] ✓ Plugin loaded: {plugin_basename}")
loaded_count += 1
_LOADED_PLUGINS[plugin_basename] = plugin_path
except Exception:
try:
# Fallback: load by basename
cmds.loadPlugin(plugin_basename, quiet=True)
_tool_print(f"[Tool] ✓ Plugin loaded: {plugin_basename}")
loaded_count += 1
_LOADED_PLUGINS[plugin_basename] = plugin_path
except Exception as e:
_tool_print(f"[Tool] Error loading plugin {plugin_basename}: {e}")
_tool_log(f"[Tool] ✓ Plugins: {loaded_count}/{len(plugin_configs)} loaded")
except Exception as e:
print(f"[Nexus] Error in load_nexus_plugins: {e}")
_tool_print(f"[Tool] Error loading plugins: {e}")
def load_tool_scripts():
"""Add Nexus script paths to sys.path"""
try:
script_dir = _get_script_dir()
script_configs = TOOL_CONFIG.get('scripts', [])
if not script_configs:
_tool_log("[Tool] No scripts configured")
return
global _ADDED_SCRIPT_PATHS
loaded = 0
for cfg in script_configs:
name = cfg.get('name', '')
path = cfg.get('path', '')
if not (name and path):
continue
full_path = os.path.normpath(os.path.join(script_dir, path))
if not os.path.exists(full_path):
_tool_print(f"[Tool] Script path not found: {full_path}")
continue
if full_path not in sys.path:
sys.path.insert(0, full_path)
_ADDED_SCRIPT_PATHS.append(full_path)
_tool_log(f"[Tool] Added script path: {name} -> {full_path}")
loaded += 1
_tool_log(f"[Tool] ✓ Script paths: {loaded}/{len(script_configs)} added")
except Exception as e:
_tool_print(f"[Tool] Error loading scripts: {e}")
def setup_command_port():
"""Setup command port for external connections"""
global _COMMAND_PORT_OPENED
try:
# Check if already open
try:
port_exists = bool(mel.eval('commandPort -q -name "commandportDefault"'))
except Exception:
port_exists = False
if port_exists:
_tool_log("[Tool] Command port already open")
_COMMAND_PORT_OPENED = True
return
# Open command port
mel.eval('commandPort -securityWarning -name "commandportDefault";')
_tool_log("[Tool] ✓ Command port opened")
_COMMAND_PORT_OPENED = True
except Exception as e:
_tool_log(f"[Tool] Could not open command port: {e}")
# =============================================================================
# Main Initialization
# =============================================================================
def initialize_tool():
"""Main initialization function called on Maya startup"""
try:
print("=" * 80)
print("[Tool] Nexus Plugin System - Initializing...")
print("=" * 80)
# Check Maya version compatibility
if not _check_maya_version():
print("[Tool] Warning: Running on unsupported Maya version")
# Setup command port
setup_command_port()
# Load components in order
setup_icon_paths()
load_tool_scripts()
load_tool_plugins()
load_tool_shelves()
print("=" * 80)
print("[Tool] Nexus Plugin System - Ready!")
print("=" * 80)
except Exception as e:
print(f"[Tool] Initialization error: {e}")
import traceback
traceback.print_exc()
# Defer initialization until Maya is fully loaded
maya.utils.executeDeferred(initialize_tool)
# Deferred execution
def initialize_nexus():
"""Initialize Nexus plugin system"""
print("=" * 80)
print("[Nexus] Initializing Nexus Maya 2023 Plugin System...")
print("=" * 80)
load_nexus_shelves()
load_nexus_plugins()
print("=" * 80)
print("[Nexus] Nexus Plugin System Initialized!")
print("=" * 80)
# =============================================================================
# Cleanup on Exit
# =============================================================================
# Execute after Maya is fully loaded
maya.utils.executeDeferred(initialize_nexus)
# Cleanup on exit
def cleanup_on_exit():
"""Cleanup when Maya exits"""
print("[Nexus] Cleaning up...")
# Unload plugins
plugins_to_unload = ["nexus_plugin.py"]
for plugin_name in plugins_to_unload:
if cmds.pluginInfo(plugin_name, query=True, loaded=True):
"""Cleanup resources when Maya exits"""
try:
_tool_log("[Tool] Cleanup initiated...")
# Close command port
global _COMMAND_PORT_OPENED
if _COMMAND_PORT_OPENED:
try:
cmds.unloadPlugin(plugin_name)
print(f"[Nexus] Plugin unloaded: {plugin_name}")
except:
pass
if mel.eval('commandPort -q -name "commandportDefault"'):
mel.eval('commandPort -cl "commandportDefault"')
_tool_log("[Tool] ✓ Command port closed")
except Exception as e:
_tool_log(f"[Tool] Could not close command port: {e}")
# Unload plugins
if _LOADED_PLUGINS:
unloaded = 0
for plugin_basename, plugin_path in list(_LOADED_PLUGINS.items()):
try:
# Check if still loaded
try:
is_loaded = bool(cmds.pluginInfo(plugin_basename, query=True, loaded=True))
except Exception:
is_loaded = False
if is_loaded:
try:
cmds.unloadPlugin(plugin_basename, force=True)
_tool_log(f"[Tool] ✓ Plugin unloaded: {plugin_basename}")
unloaded += 1
except Exception as e:
_tool_log(f"[Tool] Could not unload plugin {plugin_basename}: {e}")
except Exception:
continue
if unloaded > 0:
_tool_log(f"[Tool] ✓ Plugins unloaded: {unloaded}/{len(_LOADED_PLUGINS)}")
import atexit
atexit.register(cleanup_on_exit)
# Clean up script paths from sys.path
global _ADDED_SCRIPT_PATHS
if _ADDED_SCRIPT_PATHS:
for path in _ADDED_SCRIPT_PATHS:
try:
if path in sys.path:
sys.path.remove(path)
except Exception:
pass
_tool_log(f"[Tool] ✓ Script paths cleaned: {len(_ADDED_SCRIPT_PATHS)}")
# Clear global state
_LOADED_PLUGINS.clear()
_ICONS_PATHS.clear()
_ADDED_SCRIPT_PATHS.clear()
_tool_log("[Tool] ✓ Cleanup complete")
except Exception as e:
_tool_log(f"[Tool] Cleanup error: {e}")
# Register cleanup function
atexit.register(cleanup_on_exit)