Skip to content

Commit

Permalink
Merge pull request #120 from GenEugene/chain-distribution-rig
Browse files Browse the repository at this point in the history
Chain Distribution Rig
  • Loading branch information
GenEugene authored Nov 20, 2024
2 parents 9d26275 + fbf96dc commit e80ad2d
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 40 deletions.
7 changes: 6 additions & 1 deletion GETOOLS_SOURCE/modules/GeneralWindow.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@


class GeneralWindow:
_version = "v1.4.4"
_version = "v1.5.0"
_name = "GETools"
_title = _name + " " + _version

Expand Down Expand Up @@ -277,6 +277,11 @@ def LayoutMenuOptions(self):
cmds.menuItem(label = "Without Reverse Constraint", command = partial(Install.ToShelf_LocatorsRelativeWithoutReverse, self.optionsPlugin.directory))
cmds.setParent('..', menu = True)
#
cmds.menuItem(subMenu = True, label = "Chain Distribution", tearOff = True, image = Icons.pinInvert)
cmds.menuItem(label = "Default Mode", command = partial(Install.ToShelf_LocatorsChainDistribution1, self.optionsPlugin.directory))
cmds.menuItem(label = "Alternative Mode", command = partial(Install.ToShelf_LocatorsChainDistribution2, self.optionsPlugin.directory))
cmds.setParent('..', menu = True)
#
cmds.menuItem(subMenu = True, label = "Aim", tearOff = True, image = Icons.pin)
minus = "-"
plus = "+"
Expand Down
3 changes: 2 additions & 1 deletion GETOOLS_SOURCE/modules/Overlappy.py
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,7 @@ def UpdateParticleAllSettings(self, *args):
self.UpdateParticleAimOffsetSettings()
self.UpdateParticleSettings()
def UpdateParticleAimOffsetSettings(self, *args):
if (self.setupCreatedPoint):
if (not self.setupCreated or self.setupCreatedPoint):
return

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

self.time.SetCurrent(self.time.values[2])

SetParticleAimOffset(nameLocator = self.particleLocatorGoalOffset, nameParticle = self.particleTarget, goalStartPosition = self.particleLocatorGoalOffsetStartPosition, offset = self.particleAimOffsetTarget)
SetParticleAimOffset(nameLocator = self.particleLocatorGoalOffsetUp, nameParticle = self.particleUp, goalStartPosition = self.particleLocatorGoalOffsetUpStartPosition, offset = self.particleAimOffsetUp)
def UpdateParticleSettings(self, *args):
Expand Down
52 changes: 34 additions & 18 deletions GETOOLS_SOURCE/modules/Tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from .. import Settings
from ..utils import Animation
from ..utils import Baker
from ..utils import ChainDistributionRig
from ..utils import Colors
from ..utils import Locators
from ..utils import Selector
Expand Down Expand Up @@ -62,6 +63,8 @@ class ToolsAnnotations:
#
locatorsRelative = "{bake}\nThe last locator becomes the parent of other locators".format(bake = locatorsBake)
locatorsRelativeReverse = "{relative}\n{reverse}\nRight click allows you to bake the same operation but with constrained last object.".format(relative = locatorsRelative, reverse = _reverseConstraint)
#
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."

# locatorAimSpace = "Locator Aim distance from original object. Need to use non-zero value"
locatorAimSpace = "Aim Space offset from original object.\nNeed to use non-zero value to get best result"
Expand Down Expand Up @@ -104,13 +107,15 @@ class ToolsAnnotations:
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."

class ToolsSettings:
### AIM SPACE
locatorSize = 10

### Aim Space
aimSpaceName = "Offset"
aimSpaceOffsetValue = 100
aimSpaceRadioButtonDefault = 0

class Tools:
_version = "v1.3"
_version = "v1.4"
_name = "TOOLS"
_title = _name + " " + _version

Expand All @@ -125,14 +130,12 @@ def __init__(self, options):
self.checkboxLocatorHideParent = None
self.checkboxLocatorSubLocator = None
self.floatLocatorSize = None

### Animation Offset
self.animOffsetFloatField = None

### Locator Aim Space
self.aimSpaceFloatField = None
self.aimSpaceRadioButtons = [None, None, None]
self.aimSpaceCheckbox = None
### Animation Offset
self.animOffsetFloatField = None

self.bakingSamplesValue = None

Expand All @@ -144,7 +147,8 @@ def UICreate(self, layoutMain):
def UILayoutLocators(self, layoutMain):
layoutLocators = cmds.frameLayout(parent = layoutMain, label = Settings.frames2Prefix + "LOCATORS // SPACE SWITCHING", collapsable = True, backgroundColor = Settings.frames2Color, marginWidth = 0, marginHeight = 0)
layoutColumn = cmds.columnLayout(parent = layoutLocators, adjustableColumn = True)
#

### LOCATORS SIZE
countOffsets = 6
cellWidth = Settings.windowWidthMargin / countOffsets
cmds.gridLayout(parent = layoutColumn, numberOfColumns = countOffsets, cellWidth = cellWidth, cellHeight = Settings.lineHeight)
Expand Down Expand Up @@ -182,13 +186,15 @@ def UILayoutLocators(self, layoutMain):
cmds.menuItem(divider = True)
cmds.menuItem(label = "1000", command = partial(Locators.SelectedLocatorsSizeSet, 1000))
cmds.menuItem(label = "5000", command = partial(Locators.SelectedLocatorsSizeSet, 5000))
#

### OPTIONS
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)])
self.checkboxLocatorHideParent = cmds.checkBox(label = "Hide Parent", value = False, annotation = ToolsAnnotations.hideParent)
self.checkboxLocatorSubLocator = cmds.checkBox(label = "Sub Locator", value = False, annotation = ToolsAnnotations.subLocator)
cmds.text(label = "Size:", annotation = ToolsAnnotations.locatorSize)
self.floatLocatorSize = cmds.floatField(value = 10, precision = 3, annotation = ToolsAnnotations.locatorSize)
#
self.floatLocatorSize = cmds.floatField(value = ToolsSettings.locatorSize, precision = 3, annotation = ToolsAnnotations.locatorSize)

### LOCATORS ROW 1
countOffsets = 6
cmds.gridLayout(parent = layoutColumn, numberOfColumns = countOffsets, cellWidth = Settings.windowWidthMargin / countOffsets, cellHeight = Settings.lineHeight)
cmds.button(label = "Locator", command = self.Locator, backgroundColor = Colors.green10, annotation = ToolsAnnotations.locator)
Expand All @@ -199,18 +205,20 @@ def UILayoutLocators(self, layoutMain):
cmds.menuItem(label = "Without Reverse Constraint", command = self.LocatorsBake)
cmds.button(label = "P-POS", command = partial(self.LocatorsBakeReverse, True, False), backgroundColor = Colors.yellow50, annotation = ToolsAnnotations.locatorsBakeReversePos)
cmds.button(label = "P-ROT", command = partial(self.LocatorsBakeReverse, False, True), backgroundColor = Colors.yellow50, annotation = ToolsAnnotations.locatorsBakeReverseRot)
#
countOffsets = 1
cmds.gridLayout(parent = layoutColumn, numberOfColumns = countOffsets, cellWidth = Settings.windowWidthMargin / countOffsets, cellHeight = Settings.lineHeight)

### LOCATORS ROW 2
cmds.rowLayout(parent = layoutColumn, numberOfColumns = 2, columnWidth2 = (113, 160), columnAlign = [(1, "center"), (2, "center")], columnAttach = [(1, "both", 0), (2, "both", 0)])
cmds.button(label = "Relative", command = self.LocatorsRelativeReverse, backgroundColor = Colors.orange10, annotation = ToolsAnnotations.locatorsRelativeReverse)
cmds.popupMenu()
cmds.menuItem(label = "Skip Last Object Reverse Constraint", command = self.LocatorsRelativeReverseSkipLast)
cmds.menuItem(label = "Without Reverse Constraint", command = self.LocatorsRelative)
#

### Aim Space Switching
layoutAimSpace = cmds.frameLayout(parent = layoutColumn, label = "Aim Space Switching", labelIndent = 75, collapsable = False, backgroundColor = Settings.frames2Color, marginWidth = 0, marginHeight = 0)
cmds.button(label = "Chain Distribution", command = partial(self.CreateChainDistributionRig, 1), backgroundColor = Colors.purple10, annotation = ToolsAnnotations.chainDistribution)
cmds.popupMenu()
cmds.menuItem(label = "Alternative Mode", command = partial(self.CreateChainDistributionRig, 2))

### AIM SPACE SWITCHING
layoutAimSpace = cmds.frameLayout(parent = layoutColumn, label = "Aim Space Switching", labelIndent = 75, collapsable = False, backgroundColor = Settings.frames2Color, marginWidth = 0, marginHeight = 0)
#
cmds.rowLayout(parent = layoutAimSpace, numberOfColumns = 6, columnWidth6 = (40, 55, 35, 35, 35, 60), columnAlign = [1, "center"], columnAttach = [(1, "both", 0)])
cmds.text(label = ToolsSettings.aimSpaceName)
self.aimSpaceFloatField = cmds.floatField(value = ToolsSettings.aimSpaceOffsetValue, precision = 3, minValue = 0, annotation = ToolsAnnotations.locatorAimSpace)
Expand All @@ -220,7 +228,7 @@ def UILayoutLocators(self, layoutMain):
self.aimSpaceRadioButtons[2] = cmds.radioButton(label = "Z")
self.aimSpaceCheckbox = cmds.checkBox(label = "Reverse", value = False)
cmds.radioButton(self.aimSpaceRadioButtons[ToolsSettings.aimSpaceRadioButtonDefault], edit = True, select = True)

#
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)])
cmds.text(label = "Create")
cmds.button(label = "Translate + Rotate", command = partial(self.LocatorsBakeAim, False), backgroundColor = Colors.orange10, annotation = ToolsAnnotations.locatorAimSpaceBakeAll)
Expand Down Expand Up @@ -313,6 +321,7 @@ def UILayoutTimeline(self, layoutMain):
cmds.button(label = ">-<", command = partial(Timeline.SetTime, 6), backgroundColor = Colors.orange10, annotation = ToolsAnnotations.timelineFocusIn)
cmds.button(label = "|<->|", command = partial(Timeline.SetTime, 7), backgroundColor = Colors.orange50, annotation = ToolsAnnotations.timelineSetRange)


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

### CHAIN DISTRIBUTION RIG
def CreateChainDistributionRig(self, mode=1, *args):
if mode is 1:
ChainDistributionRig.CreateRigVariant1(locatorSize = self.GetFloatLocatorSize())
if mode is 2:
ChainDistributionRig.CreateRigVariant2(locatorSize = self.GetFloatLocatorSize())


### BAKING
def BakeSampleGet(self):
Expand Down
215 changes: 215 additions & 0 deletions GETOOLS_SOURCE/utils/ChainDistributionRig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
# GETOOLS is under the terms of the MIT License
# Copyright (c) 2018-2024 Eugene Gataulin (GenEugene). All Rights Reserved.

# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:

# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

# Author: Eugene Gataulin tek942@gmail.com https://www.linkedin.com/in/geneugene
# Source code: https://github.com/GenEugene/GETools or https://app.gumroad.com/geneugene

import maya.cmds as cmds

from ..utils import Selector
from ..utils import Text


_locatorSize = 100
_nameGroupMain = "grpChain_"
_nameLocatorPrefix = "loc_"


def CreateRigVariant1(locatorSize=_locatorSize, *args):
# Check selected objects
selectedList = Selector.MultipleObjects(minimalCount = 1)
if (selectedList == None):
return

timeCurrent = cmds.currentTime(query = True)
timeMin = cmds.playbackOptions(query = True, min = True)
timeMax = cmds.playbackOptions(query = True, max = True)
cmds.currentTime(timeMin, edit = True, update = True)

### Create a list of names from selected objects
selected = cmds.ls(selection = True)

### Create main group as a container for all new objects
mainGroup = cmds.group(name = Text.SetUniqueFromText(_nameGroupMain + selected[-1]), empty = True)

### Init empty lists for groups and locators
locators = []
constraintsForBake = []

### Count of selected objects
count = len(selected)

### Loop through each selected object, create groups, locators and parent them
for i in range(count):
### Create locator # TODO nurbs circle as control instead of locator (optional)
locator = cmds.spaceLocator(name = Text.SetUniqueFromText(_nameLocatorPrefix + selected[i]))[0]
locators.append(locator)
cmds.setAttr(locator + "Shape.localScaleX", locatorSize)
cmds.setAttr(locator + "Shape.localScaleY", locatorSize)
cmds.setAttr(locator + "Shape.localScaleZ", locatorSize)

### Parent locator to group
cmds.parent(locator, mainGroup)

### Match group position and rotation
cmds.matchTransform(locator, selected[i], position = True, rotation = True, scale = False)

### Parent constraint groupFixed to original object
constraint = cmds.parentConstraint(selected[i], locator, maintainOffset = False)
constraintsForBake.append(constraint[0])

### Bake animation to locators and delete constraints
cmds.bakeResults(locators, time = (timeMin, timeMax), simulation = True, minimizeRotation = True)
cmds.delete(constraintsForBake)

### Constrain
for i in range(count):
cmds.pointConstraint(selected[i], locators[i], maintainOffset = False)
cmds.orientConstraint(locators[i], selected[i], maintainOffset = False)

if (i > 0 and i < count - 1):
cmds.orientConstraint(locators[0], locators[i], maintainOffset = True)
cmds.orientConstraint(locators[i + 1], locators[i], maintainOffset = True)

### Select last locator
cmds.select(locators[-1], replace = True)
cmds.currentTime(timeCurrent, edit = True, update = True)

def CreateRigVariant2(locatorSize=_locatorSize, *args):
# Check selected objects
selectedList = Selector.MultipleObjects(minimalCount = 1)
if (selectedList == None):
return

### Objects names
nameGroupFixedPrefix = "grpFixed_"
nameGroupDistributedPrefix = "grpDistr_"
### Attributes names
nameAttributeWeight = "distribution"
nameAttributeGlobal = "global"
### Nodes names
nameMultiplyDivide = "gtMultiplyDivide"

### Create a list of names from selected objects
selected = cmds.ls(selection = True)

### Create main group as a container for all new objects
mainGroup = cmds.group(name = Text.SetUniqueFromText(_nameGroupMain + selected[-1]), empty = True)

### Init empty lists for groups and locators
groupsFixed = []
groupsDistributed = []
locators = []
constraintsForBake = []

### Count of selected objects
count = len(selected)

### Loop through each selected object, create groups, locators and parent them
for i in range(count):
### Create fixed group
groupFixed = cmds.group(name = Text.SetUniqueFromText(nameGroupFixedPrefix + selected[i]), empty = True)
groupsFixed.append(groupFixed)

### Create distribution group
groupDistributed = cmds.group(name = Text.SetUniqueFromText(nameGroupDistributedPrefix + selected[i]), empty = True)
groupsDistributed.append(groupDistributed)

### 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;]
locator = cmds.spaceLocator(name = Text.SetUniqueFromText(_nameLocatorPrefix + selected[i]))[0]
locators.append(locator)
cmds.setAttr(locator + "Shape.localScaleX", locatorSize)
cmds.setAttr(locator + "Shape.localScaleY", locatorSize)
cmds.setAttr(locator + "Shape.localScaleZ", locatorSize)

### Parent locator to group
cmds.parent(locator, groupDistributed)

### Parent group to corresponding hierarchy object
if i == 0:
cmds.parent(groupFixed, mainGroup)
else:
cmds.parent(groupFixed, locators[i - 1])
cmds.parent(groupDistributed, groupFixed)

### Match group position and rotation
cmds.matchTransform(groupFixed, selected[i], position = True, rotation = True, scale = False)

### Parent constraint groupFixed to original object
constraint = cmds.parentConstraint(selected[i], groupFixed, maintainOffset = True)
constraintsForBake.append(constraint[0])

### Bake animation to locators and delete constraints
timeMin = cmds.playbackOptions(query = True, min = True)
timeMax = cmds.playbackOptions(query = True, max = True)
cmds.bakeResults(groupsFixed, time = (timeMin, timeMax), simulation = True, minimizeRotation = True)
cmds.delete(constraintsForBake)

### Parent constraint original objects to locators
for i in range(count):
cmds.parentConstraint(locators[i], selected[i], maintainOffset = True)

### Show last locator Rotate Order and connect it to Distribution groups
cmds.setAttr(locators[-1] + ".rotateOrder", channelBox = True)
for i in range(count):
groupsDistributed[i]
cmds.connectAttr(locators[-1] + ".rotateOrder", groupsDistributed[i] + ".rotateOrder")

### Check if selected count less than 3 objects and break function
if (count < 3):
cmds.warning("You have less than 3 objects selected. Rotation distribution will not be created")
return

### Create weight attribute on last locator
cmds.addAttr(locators[-1], longName = nameAttributeWeight, attributeType = "double", defaultValue = count - 1)
cmds.setAttr(locators[-1] + "." + nameAttributeWeight, edit = True, keyable = True)

### Create MultiplyDivide node
nodeMultiplyDivide = cmds.createNode("multiplyDivide", name = Text.SetUniqueFromText(nameMultiplyDivide))
cmds.setAttr(nodeMultiplyDivide + ".operation", 2)

### Connect rotation and weight to MultiplyDivide node
cmds.connectAttr(locators[-1] + ".rotate", nodeMultiplyDivide + ".input1")
cmds.connectAttr(locators[-1] + "." + nameAttributeWeight, nodeMultiplyDivide + ".input2X")
cmds.connectAttr(locators[-1] + "." + nameAttributeWeight, nodeMultiplyDivide + ".input2Y")
cmds.connectAttr(locators[-1] + "." + nameAttributeWeight, nodeMultiplyDivide + ".input2Z")

### Connect rotation distribution to other locators' groups
for i in range(1, count - 1):
cmds.connectAttr(nodeMultiplyDivide + ".output", groupsDistributed[i] + ".rotate")

### Add global attribute for last locator
cmds.addAttr(locators[-1], longName = nameAttributeGlobal, attributeType = "double", defaultValue = 0, minValue = 0, maxValue = 1)
cmds.setAttr(locators[-1] + "." + nameAttributeGlobal, edit = True, keyable = True)

### Create Orient Constraint for last locator
cmds.orientConstraint(mainGroup, groupsDistributed[-1], maintainOffset = True)[0]

### Show blend orient attribute by setting keys on constrained rotation attributes # I frankly don't know how to do it better
cmds.setKeyframe(groupsDistributed[-1] + ".rx")
cmds.setKeyframe(groupsDistributed[-1] + ".ry")
cmds.setKeyframe(groupsDistributed[-1] + ".rz")

### Connect Global attribute to blend orient attribute
cmds.connectAttr(locators[-1] + "." + nameAttributeGlobal, groupsDistributed[-1] + ".blendOrient1")

### Select last locator
cmds.select(locators[-1], replace = True)

Loading

0 comments on commit e80ad2d

Please sign in to comment.