Skip to content

Commit e80ad2d

Browse files
authored
Merge pull request #120 from GenEugene/chain-distribution-rig
Chain Distribution Rig
2 parents 9d26275 + fbf96dc commit e80ad2d

File tree

10 files changed

+301
-40
lines changed

10 files changed

+301
-40
lines changed

GETOOLS_SOURCE/modules/GeneralWindow.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949

5050

5151
class GeneralWindow:
52-
_version = "v1.4.4"
52+
_version = "v1.5.0"
5353
_name = "GETools"
5454
_title = _name + " " + _version
5555

@@ -277,6 +277,11 @@ def LayoutMenuOptions(self):
277277
cmds.menuItem(label = "Without Reverse Constraint", command = partial(Install.ToShelf_LocatorsRelativeWithoutReverse, self.optionsPlugin.directory))
278278
cmds.setParent('..', menu = True)
279279
#
280+
cmds.menuItem(subMenu = True, label = "Chain Distribution", tearOff = True, image = Icons.pinInvert)
281+
cmds.menuItem(label = "Default Mode", command = partial(Install.ToShelf_LocatorsChainDistribution1, self.optionsPlugin.directory))
282+
cmds.menuItem(label = "Alternative Mode", command = partial(Install.ToShelf_LocatorsChainDistribution2, self.optionsPlugin.directory))
283+
cmds.setParent('..', menu = True)
284+
#
280285
cmds.menuItem(subMenu = True, label = "Aim", tearOff = True, image = Icons.pin)
281286
minus = "-"
282287
plus = "+"

GETOOLS_SOURCE/modules/Overlappy.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,7 @@ def UpdateParticleAllSettings(self, *args):
708708
self.UpdateParticleAimOffsetSettings()
709709
self.UpdateParticleSettings()
710710
def UpdateParticleAimOffsetSettings(self, *args):
711-
if (self.setupCreatedPoint):
711+
if (not self.setupCreated or self.setupCreatedPoint):
712712
return
713713

714714
def SetParticleAimOffset(nameLocator, nameParticle, goalStartPosition, offset=(0, 0, 0)):
@@ -726,6 +726,7 @@ def SetParticleAimOffset(nameLocator, nameParticle, goalStartPosition, offset=(0
726726
self.CompileParticleAimOffset()
727727

728728
self.time.SetCurrent(self.time.values[2])
729+
729730
SetParticleAimOffset(nameLocator = self.particleLocatorGoalOffset, nameParticle = self.particleTarget, goalStartPosition = self.particleLocatorGoalOffsetStartPosition, offset = self.particleAimOffsetTarget)
730731
SetParticleAimOffset(nameLocator = self.particleLocatorGoalOffsetUp, nameParticle = self.particleUp, goalStartPosition = self.particleLocatorGoalOffsetUpStartPosition, offset = self.particleAimOffsetUp)
731732
def UpdateParticleSettings(self, *args):

GETOOLS_SOURCE/modules/Tools.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from .. import Settings
2828
from ..utils import Animation
2929
from ..utils import Baker
30+
from ..utils import ChainDistributionRig
3031
from ..utils import Colors
3132
from ..utils import Locators
3233
from ..utils import Selector
@@ -62,6 +63,8 @@ class ToolsAnnotations:
6263
#
6364
locatorsRelative = "{bake}\nThe last locator becomes the parent of other locators".format(bake = locatorsBake)
6465
locatorsRelativeReverse = "{relative}\n{reverse}\nRight click allows you to bake the same operation but with constrained last object.".format(relative = locatorsRelative, reverse = _reverseConstraint)
66+
#
67+
chainDistribution = "Create a chain with distributed rotation. Use the last locator to animate.\nWorks better with 3 selected objects.\nIf you select 4+ objects, the original animation will not be fully preserved.\n\nRight-click to use the alternate mode to preserve 100% of the original animation with any number of selected objects.\nIt is not as convenient to use as the default mode."
6568

6669
# locatorAimSpace = "Locator Aim distance from original object. Need to use non-zero value"
6770
locatorAimSpace = "Aim Space offset from original object.\nNeed to use non-zero value to get best result"
@@ -104,13 +107,15 @@ class ToolsAnnotations:
104107
animationOffset = "Move animation curves on selected objects.\nAnimation will move relative to the index of the selected object.\nThe best way to desync animation.\nWorks with selection in the channel box."
105108

106109
class ToolsSettings:
107-
### AIM SPACE
110+
locatorSize = 10
111+
112+
### Aim Space
108113
aimSpaceName = "Offset"
109114
aimSpaceOffsetValue = 100
110115
aimSpaceRadioButtonDefault = 0
111116

112117
class Tools:
113-
_version = "v1.3"
118+
_version = "v1.4"
114119
_name = "TOOLS"
115120
_title = _name + " " + _version
116121

@@ -125,14 +130,12 @@ def __init__(self, options):
125130
self.checkboxLocatorHideParent = None
126131
self.checkboxLocatorSubLocator = None
127132
self.floatLocatorSize = None
128-
129-
### Animation Offset
130-
self.animOffsetFloatField = None
131-
132133
### Locator Aim Space
133134
self.aimSpaceFloatField = None
134135
self.aimSpaceRadioButtons = [None, None, None]
135136
self.aimSpaceCheckbox = None
137+
### Animation Offset
138+
self.animOffsetFloatField = None
136139

137140
self.bakingSamplesValue = None
138141

@@ -144,7 +147,8 @@ def UICreate(self, layoutMain):
144147
def UILayoutLocators(self, layoutMain):
145148
layoutLocators = cmds.frameLayout(parent = layoutMain, label = Settings.frames2Prefix + "LOCATORS // SPACE SWITCHING", collapsable = True, backgroundColor = Settings.frames2Color, marginWidth = 0, marginHeight = 0)
146149
layoutColumn = cmds.columnLayout(parent = layoutLocators, adjustableColumn = True)
147-
#
150+
151+
### LOCATORS SIZE
148152
countOffsets = 6
149153
cellWidth = Settings.windowWidthMargin / countOffsets
150154
cmds.gridLayout(parent = layoutColumn, numberOfColumns = countOffsets, cellWidth = cellWidth, cellHeight = Settings.lineHeight)
@@ -182,13 +186,15 @@ def UILayoutLocators(self, layoutMain):
182186
cmds.menuItem(divider = True)
183187
cmds.menuItem(label = "1000", command = partial(Locators.SelectedLocatorsSizeSet, 1000))
184188
cmds.menuItem(label = "5000", command = partial(Locators.SelectedLocatorsSizeSet, 5000))
185-
#
189+
190+
### OPTIONS
186191
cmds.rowLayout(parent = layoutColumn, numberOfColumns = 4, columnWidth4 = (85, 85, 40, 60), columnAlign = [(1, "center"), (2, "center"), (3, "right"), (4, "center")], columnAttach = [(1, "both", 0), (2, "both", 0), (3, "both", 0), (4, "both", 0)])
187192
self.checkboxLocatorHideParent = cmds.checkBox(label = "Hide Parent", value = False, annotation = ToolsAnnotations.hideParent)
188193
self.checkboxLocatorSubLocator = cmds.checkBox(label = "Sub Locator", value = False, annotation = ToolsAnnotations.subLocator)
189194
cmds.text(label = "Size:", annotation = ToolsAnnotations.locatorSize)
190-
self.floatLocatorSize = cmds.floatField(value = 10, precision = 3, annotation = ToolsAnnotations.locatorSize)
191-
#
195+
self.floatLocatorSize = cmds.floatField(value = ToolsSettings.locatorSize, precision = 3, annotation = ToolsAnnotations.locatorSize)
196+
197+
### LOCATORS ROW 1
192198
countOffsets = 6
193199
cmds.gridLayout(parent = layoutColumn, numberOfColumns = countOffsets, cellWidth = Settings.windowWidthMargin / countOffsets, cellHeight = Settings.lineHeight)
194200
cmds.button(label = "Locator", command = self.Locator, backgroundColor = Colors.green10, annotation = ToolsAnnotations.locator)
@@ -199,18 +205,20 @@ def UILayoutLocators(self, layoutMain):
199205
cmds.menuItem(label = "Without Reverse Constraint", command = self.LocatorsBake)
200206
cmds.button(label = "P-POS", command = partial(self.LocatorsBakeReverse, True, False), backgroundColor = Colors.yellow50, annotation = ToolsAnnotations.locatorsBakeReversePos)
201207
cmds.button(label = "P-ROT", command = partial(self.LocatorsBakeReverse, False, True), backgroundColor = Colors.yellow50, annotation = ToolsAnnotations.locatorsBakeReverseRot)
202-
#
203-
countOffsets = 1
204-
cmds.gridLayout(parent = layoutColumn, numberOfColumns = countOffsets, cellWidth = Settings.windowWidthMargin / countOffsets, cellHeight = Settings.lineHeight)
208+
209+
### LOCATORS ROW 2
210+
cmds.rowLayout(parent = layoutColumn, numberOfColumns = 2, columnWidth2 = (113, 160), columnAlign = [(1, "center"), (2, "center")], columnAttach = [(1, "both", 0), (2, "both", 0)])
205211
cmds.button(label = "Relative", command = self.LocatorsRelativeReverse, backgroundColor = Colors.orange10, annotation = ToolsAnnotations.locatorsRelativeReverse)
206212
cmds.popupMenu()
207213
cmds.menuItem(label = "Skip Last Object Reverse Constraint", command = self.LocatorsRelativeReverseSkipLast)
208214
cmds.menuItem(label = "Without Reverse Constraint", command = self.LocatorsRelative)
209-
#
210-
211-
### Aim Space Switching
212-
layoutAimSpace = cmds.frameLayout(parent = layoutColumn, label = "Aim Space Switching", labelIndent = 75, collapsable = False, backgroundColor = Settings.frames2Color, marginWidth = 0, marginHeight = 0)
215+
cmds.button(label = "Chain Distribution", command = partial(self.CreateChainDistributionRig, 1), backgroundColor = Colors.purple10, annotation = ToolsAnnotations.chainDistribution)
216+
cmds.popupMenu()
217+
cmds.menuItem(label = "Alternative Mode", command = partial(self.CreateChainDistributionRig, 2))
213218

219+
### AIM SPACE SWITCHING
220+
layoutAimSpace = cmds.frameLayout(parent = layoutColumn, label = "Aim Space Switching", labelIndent = 75, collapsable = False, backgroundColor = Settings.frames2Color, marginWidth = 0, marginHeight = 0)
221+
#
214222
cmds.rowLayout(parent = layoutAimSpace, numberOfColumns = 6, columnWidth6 = (40, 55, 35, 35, 35, 60), columnAlign = [1, "center"], columnAttach = [(1, "both", 0)])
215223
cmds.text(label = ToolsSettings.aimSpaceName)
216224
self.aimSpaceFloatField = cmds.floatField(value = ToolsSettings.aimSpaceOffsetValue, precision = 3, minValue = 0, annotation = ToolsAnnotations.locatorAimSpace)
@@ -220,7 +228,7 @@ def UILayoutLocators(self, layoutMain):
220228
self.aimSpaceRadioButtons[2] = cmds.radioButton(label = "Z")
221229
self.aimSpaceCheckbox = cmds.checkBox(label = "Reverse", value = False)
222230
cmds.radioButton(self.aimSpaceRadioButtons[ToolsSettings.aimSpaceRadioButtonDefault], edit = True, select = True)
223-
231+
#
224232
cmds.rowLayout(parent = layoutAimSpace, numberOfColumns = 3, columnWidth3 = (50, 110, 110), columnAlign = [(1, "center"), (2, "center"), (3, "center")], columnAttach = [(1, "both", 0), (2, "both", 0), (3, "both", 0)])
225233
cmds.text(label = "Create")
226234
cmds.button(label = "Translate + Rotate", command = partial(self.LocatorsBakeAim, False), backgroundColor = Colors.orange10, annotation = ToolsAnnotations.locatorAimSpaceBakeAll)
@@ -313,6 +321,7 @@ def UILayoutTimeline(self, layoutMain):
313321
cmds.button(label = ">-<", command = partial(Timeline.SetTime, 6), backgroundColor = Colors.orange10, annotation = ToolsAnnotations.timelineFocusIn)
314322
cmds.button(label = "|<->|", command = partial(Timeline.SetTime, 7), backgroundColor = Colors.orange50, annotation = ToolsAnnotations.timelineSetRange)
315323

324+
316325
### LOCATORS
317326
def GetFloatLocatorSize(self):
318327
return cmds.floatField(self.floatLocatorSize, query = True, value = True)
@@ -393,6 +402,13 @@ def LocatorsBakeAim(self, rotateOnly=False, *args):
393402
if (distance == 0):
394403
cmds.warning("Aim distance is 0. Highly recommended to use non-zero value.")
395404

405+
### CHAIN DISTRIBUTION RIG
406+
def CreateChainDistributionRig(self, mode=1, *args):
407+
if mode is 1:
408+
ChainDistributionRig.CreateRigVariant1(locatorSize = self.GetFloatLocatorSize())
409+
if mode is 2:
410+
ChainDistributionRig.CreateRigVariant2(locatorSize = self.GetFloatLocatorSize())
411+
396412

397413
### BAKING
398414
def BakeSampleGet(self):
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# GETOOLS is under the terms of the MIT License
2+
# Copyright (c) 2018-2024 Eugene Gataulin (GenEugene). All Rights Reserved.
3+
4+
# Permission is hereby granted, free of charge, to any person obtaining a copy
5+
# of this software and associated documentation files (the "Software"), to deal
6+
# in the Software without restriction, including without limitation the rights
7+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
# copies of the Software, and to permit persons to whom the Software is
9+
# furnished to do so, subject to the following conditions:
10+
11+
# The above copyright notice and this permission notice shall be included in all
12+
# copies or substantial portions of the Software.
13+
14+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20+
21+
# Author: Eugene Gataulin [email protected] https://www.linkedin.com/in/geneugene
22+
# Source code: https://github.com/GenEugene/GETools or https://app.gumroad.com/geneugene
23+
24+
import maya.cmds as cmds
25+
26+
from ..utils import Selector
27+
from ..utils import Text
28+
29+
30+
_locatorSize = 100
31+
_nameGroupMain = "grpChain_"
32+
_nameLocatorPrefix = "loc_"
33+
34+
35+
def CreateRigVariant1(locatorSize=_locatorSize, *args):
36+
# Check selected objects
37+
selectedList = Selector.MultipleObjects(minimalCount = 1)
38+
if (selectedList == None):
39+
return
40+
41+
timeCurrent = cmds.currentTime(query = True)
42+
timeMin = cmds.playbackOptions(query = True, min = True)
43+
timeMax = cmds.playbackOptions(query = True, max = True)
44+
cmds.currentTime(timeMin, edit = True, update = True)
45+
46+
### Create a list of names from selected objects
47+
selected = cmds.ls(selection = True)
48+
49+
### Create main group as a container for all new objects
50+
mainGroup = cmds.group(name = Text.SetUniqueFromText(_nameGroupMain + selected[-1]), empty = True)
51+
52+
### Init empty lists for groups and locators
53+
locators = []
54+
constraintsForBake = []
55+
56+
### Count of selected objects
57+
count = len(selected)
58+
59+
### Loop through each selected object, create groups, locators and parent them
60+
for i in range(count):
61+
### Create locator # TODO nurbs circle as control instead of locator (optional)
62+
locator = cmds.spaceLocator(name = Text.SetUniqueFromText(_nameLocatorPrefix + selected[i]))[0]
63+
locators.append(locator)
64+
cmds.setAttr(locator + "Shape.localScaleX", locatorSize)
65+
cmds.setAttr(locator + "Shape.localScaleY", locatorSize)
66+
cmds.setAttr(locator + "Shape.localScaleZ", locatorSize)
67+
68+
### Parent locator to group
69+
cmds.parent(locator, mainGroup)
70+
71+
### Match group position and rotation
72+
cmds.matchTransform(locator, selected[i], position = True, rotation = True, scale = False)
73+
74+
### Parent constraint groupFixed to original object
75+
constraint = cmds.parentConstraint(selected[i], locator, maintainOffset = False)
76+
constraintsForBake.append(constraint[0])
77+
78+
### Bake animation to locators and delete constraints
79+
cmds.bakeResults(locators, time = (timeMin, timeMax), simulation = True, minimizeRotation = True)
80+
cmds.delete(constraintsForBake)
81+
82+
### Constrain
83+
for i in range(count):
84+
cmds.pointConstraint(selected[i], locators[i], maintainOffset = False)
85+
cmds.orientConstraint(locators[i], selected[i], maintainOffset = False)
86+
87+
if (i > 0 and i < count - 1):
88+
cmds.orientConstraint(locators[0], locators[i], maintainOffset = True)
89+
cmds.orientConstraint(locators[i + 1], locators[i], maintainOffset = True)
90+
91+
### Select last locator
92+
cmds.select(locators[-1], replace = True)
93+
cmds.currentTime(timeCurrent, edit = True, update = True)
94+
95+
def CreateRigVariant2(locatorSize=_locatorSize, *args):
96+
# Check selected objects
97+
selectedList = Selector.MultipleObjects(minimalCount = 1)
98+
if (selectedList == None):
99+
return
100+
101+
### Objects names
102+
nameGroupFixedPrefix = "grpFixed_"
103+
nameGroupDistributedPrefix = "grpDistr_"
104+
### Attributes names
105+
nameAttributeWeight = "distribution"
106+
nameAttributeGlobal = "global"
107+
### Nodes names
108+
nameMultiplyDivide = "gtMultiplyDivide"
109+
110+
### Create a list of names from selected objects
111+
selected = cmds.ls(selection = True)
112+
113+
### Create main group as a container for all new objects
114+
mainGroup = cmds.group(name = Text.SetUniqueFromText(_nameGroupMain + selected[-1]), empty = True)
115+
116+
### Init empty lists for groups and locators
117+
groupsFixed = []
118+
groupsDistributed = []
119+
locators = []
120+
constraintsForBake = []
121+
122+
### Count of selected objects
123+
count = len(selected)
124+
125+
### Loop through each selected object, create groups, locators and parent them
126+
for i in range(count):
127+
### Create fixed group
128+
groupFixed = cmds.group(name = Text.SetUniqueFromText(nameGroupFixedPrefix + selected[i]), empty = True)
129+
groupsFixed.append(groupFixed)
130+
131+
### Create distribution group
132+
groupDistributed = cmds.group(name = Text.SetUniqueFromText(nameGroupDistributedPrefix + selected[i]), empty = True)
133+
groupsDistributed.append(groupDistributed)
134+
135+
### Create locator # TODO use nurbs circle [circle -c 0 0 0 -nr 0 1 0 -sw 360 -r 1 -d 3 -ut 0 -tol 1e-05 -s 8 -ch 1; objectMoveCommand;]
136+
locator = cmds.spaceLocator(name = Text.SetUniqueFromText(_nameLocatorPrefix + selected[i]))[0]
137+
locators.append(locator)
138+
cmds.setAttr(locator + "Shape.localScaleX", locatorSize)
139+
cmds.setAttr(locator + "Shape.localScaleY", locatorSize)
140+
cmds.setAttr(locator + "Shape.localScaleZ", locatorSize)
141+
142+
### Parent locator to group
143+
cmds.parent(locator, groupDistributed)
144+
145+
### Parent group to corresponding hierarchy object
146+
if i == 0:
147+
cmds.parent(groupFixed, mainGroup)
148+
else:
149+
cmds.parent(groupFixed, locators[i - 1])
150+
cmds.parent(groupDistributed, groupFixed)
151+
152+
### Match group position and rotation
153+
cmds.matchTransform(groupFixed, selected[i], position = True, rotation = True, scale = False)
154+
155+
### Parent constraint groupFixed to original object
156+
constraint = cmds.parentConstraint(selected[i], groupFixed, maintainOffset = True)
157+
constraintsForBake.append(constraint[0])
158+
159+
### Bake animation to locators and delete constraints
160+
timeMin = cmds.playbackOptions(query = True, min = True)
161+
timeMax = cmds.playbackOptions(query = True, max = True)
162+
cmds.bakeResults(groupsFixed, time = (timeMin, timeMax), simulation = True, minimizeRotation = True)
163+
cmds.delete(constraintsForBake)
164+
165+
### Parent constraint original objects to locators
166+
for i in range(count):
167+
cmds.parentConstraint(locators[i], selected[i], maintainOffset = True)
168+
169+
### Show last locator Rotate Order and connect it to Distribution groups
170+
cmds.setAttr(locators[-1] + ".rotateOrder", channelBox = True)
171+
for i in range(count):
172+
groupsDistributed[i]
173+
cmds.connectAttr(locators[-1] + ".rotateOrder", groupsDistributed[i] + ".rotateOrder")
174+
175+
### Check if selected count less than 3 objects and break function
176+
if (count < 3):
177+
cmds.warning("You have less than 3 objects selected. Rotation distribution will not be created")
178+
return
179+
180+
### Create weight attribute on last locator
181+
cmds.addAttr(locators[-1], longName = nameAttributeWeight, attributeType = "double", defaultValue = count - 1)
182+
cmds.setAttr(locators[-1] + "." + nameAttributeWeight, edit = True, keyable = True)
183+
184+
### Create MultiplyDivide node
185+
nodeMultiplyDivide = cmds.createNode("multiplyDivide", name = Text.SetUniqueFromText(nameMultiplyDivide))
186+
cmds.setAttr(nodeMultiplyDivide + ".operation", 2)
187+
188+
### Connect rotation and weight to MultiplyDivide node
189+
cmds.connectAttr(locators[-1] + ".rotate", nodeMultiplyDivide + ".input1")
190+
cmds.connectAttr(locators[-1] + "." + nameAttributeWeight, nodeMultiplyDivide + ".input2X")
191+
cmds.connectAttr(locators[-1] + "." + nameAttributeWeight, nodeMultiplyDivide + ".input2Y")
192+
cmds.connectAttr(locators[-1] + "." + nameAttributeWeight, nodeMultiplyDivide + ".input2Z")
193+
194+
### Connect rotation distribution to other locators' groups
195+
for i in range(1, count - 1):
196+
cmds.connectAttr(nodeMultiplyDivide + ".output", groupsDistributed[i] + ".rotate")
197+
198+
### Add global attribute for last locator
199+
cmds.addAttr(locators[-1], longName = nameAttributeGlobal, attributeType = "double", defaultValue = 0, minValue = 0, maxValue = 1)
200+
cmds.setAttr(locators[-1] + "." + nameAttributeGlobal, edit = True, keyable = True)
201+
202+
### Create Orient Constraint for last locator
203+
cmds.orientConstraint(mainGroup, groupsDistributed[-1], maintainOffset = True)[0]
204+
205+
### Show blend orient attribute by setting keys on constrained rotation attributes # I frankly don't know how to do it better
206+
cmds.setKeyframe(groupsDistributed[-1] + ".rx")
207+
cmds.setKeyframe(groupsDistributed[-1] + ".ry")
208+
cmds.setKeyframe(groupsDistributed[-1] + ".rz")
209+
210+
### Connect Global attribute to blend orient attribute
211+
cmds.connectAttr(locators[-1] + "." + nameAttributeGlobal, groupsDistributed[-1] + ".blendOrient1")
212+
213+
### Select last locator
214+
cmds.select(locators[-1], replace = True)
215+

0 commit comments

Comments
 (0)