diff --git a/CMakeLists.txt b/CMakeLists.txt index d4af44d..ccea002 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,7 @@ set(SOURCE_FILES "src/riderConstraint.h" "src/decomposeRotation.h" "src/twistTangentNode.h" + "src/twistMultiTangentNode.h" "src/pluginMain.cpp" "src/drawOverride.cpp" @@ -32,6 +33,7 @@ set(SOURCE_FILES "src/twistSplineNode.cpp" "src/riderConstraint.cpp" "src/twistTangentNode.cpp" + "src/twistMultiTangentNode.cpp" ) add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES}) diff --git a/README.md b/README.md index e5afdde..f8d6dc2 100644 --- a/README.md +++ b/README.md @@ -32,186 +32,17 @@ If you have to compile locally for windows, there's a `mayaConfigure.bat` file t Currently, the easiest way to handle building a spline and all of its required connections is to use `twistSplineBuilder.py` and run the `makeTwistSpline` function inside. You will probably have to edit this file to fit in your pipeline. -`makeTwistSpline(prefix, numCVs, numRiders, spread=1, maxParam=None)` +`makeTwistSpline(prefix, numCVs, numRiders, spread=1, maxParam=None, closed=False, singleTangentNode=True)` `prefix` is the naming prefix for all the DAG objects. `numCVs` is the total number of CVs `numRiders` is the number of evenly spaced locators riding the spline. Locators are used because joints don't show orientation. `spread` is the amount of space between each controller (cv and tangent). So a spread of 1 makes the CVs 3 apart. `maxParam` is the parameter value of the last CV. It defaults to (3 * spread * (numCVs - 1)) +`closed` is whether the spline will form a closed loop +`singleTangentNode` is whether to use the newer/simpler single node to calculate the automatic tangents -# Node Documentation - -## TwistSpline Node - -This node implements the TwistSpline, which is a dynamically re-parameterizing bezier spline that interpolates twist along its length. - -This TwistSpline can have any number of Control Vertices, and each control vertex has an incoming and outgoing tangent. The incoming tangent is ignored for the first CV, and the outgoing tangent is ignored for the last CV. - -The OutputSpline plug is a custom type specific to this set of nodes. - -The per-CV plugs control how the spline behaves and reparameterizes. The ControlVertex plug recieves the worldpace position and orientation of the CV itself. The InTangent and OutTangent are world matrices that define the bezier tangent positions and orientations for this CV. ParamValue and ParamWeight are used as a pair. ParamValue is the parameter that the currentCV will have when ParamWeight is 1.0. If ParamWeight is 0.0, then the parameter of this CV will float between it's neighbor's parameters (as a percentage of the lengths of its neighboring bezier spline segments). The parameterization algorithm works as long as any paramWeight is greater than 0. - -TwistValue and TwistWeight work in a similar way. Twists are interpolated between values that have weight, and skip those that don't. Because this is handled as an interpolated float value, twist behavior allows for arbitrary values, and can twist past 360deg multiple times along the spline. - -UseOrient is the "weight" of the ControlVertex worldspace orientation as part of the twist. Because of the limitations of Matrices and Quaternions, you can never get more than a 180 degree twist using orient behavior and popping can occur if you're not careful. - -DebugDisplay and DebugScale show the orientation frames calculated along the spline and control their size. - - -| Long Name (short name) | Type | Default | -| --- | --- | --- | -|outputSpline (os) | TwistSpline | n/a | -|    `The custom spline data output` | | | -|outputNurbsCurve (onc) | NurbsCurve | n/a | -|    `A Maya-native NURBS curve output. Note: It will reparameterize by length, but it will not include twist data` | | | -|splineLength (sl) | double | 0.0 | -|    `The total length of the spline` | | | -|splineDisplay (sd) | bool | True | -|    `Whether to draw the spline` | | | -|debugDisplay (dd) | bool | False | -|    `Whether to show the debug axes` | | | -|debugScale (ds) | double | 1.0 | -|    `The size of the debug axes` | | | -|vertexData (vd) | compound | n/a | -|    Inputs for all of the per-cv data | | | -|    inTangent (int) | matrix | Identity | -|        `The bezier tangent pointing towards the start of the spline` | | | -|    outTangent (ot) | matrix | Identity | -|        `The bezier tangent pointing towards the end of the spline` | | | -|    controlVertex (cv) | matrix | Identity | -|        `The control vertex` | | | -|    paramValue (pv) | double | 0.0 | -|        `The parameter value of this CV` | | | -|    paramWeight (pw) | double | 0.0 | -|        `Whether to use the paramValue, or float` | | | -|    twistValue (tv) | double | 0.0 | -|        `The twist value of this CV` | | | -|    twistWeight (tw) | double | 0.0 | -|        `Whether to use the twist value, or interpolate` | | | -|    useOrient (uo) | double | 0.0 | -|        `Whether to use the orientation of this CV as part of the twist` | | | - -## TwistTangent Node - -This node Controls the behavior of a single bezier tangent control. - -This node assumes the tangent being controlled is an outgoing tangent. So for incoming tangents, the "previous" and "next" cvs are swapped. - -These nodes usually come in pairs. One for each twist spline segment. The in/out linear targets are connected in a cycle between them. Don't worry, it's not an *actual* cycle as the plugs aren't connected that way internally to the node. - -| Long Name (short name) | Type | Default | -| --- | --- | --- | -|out (out)| double3 | n/a | -|    `The final output from this node. Usually connected to a tangent input on a TwistSpline node`| -|    outX (ox)| double | 0.0 | -|        `The output X component`| -|    outY (oy)| double | 0.0 | -|        `The output Y component`| -|    outZ (oz)| double | 0.0 | -|        `The output Z component`| -|smoothTan (st)| double3 | n/a | -|    `The pure smooth tangent in un-oriented CV space. Not affected by weight or auto`| -|    smoothTanX (stx)| double | 0.0 | -|        `The smoothTangent X component`| -|    smoothTanY (sty)| double | 0.0 | -|        `The smoothTangent Y component`| -|    smoothTanZ (stz)| double | 0.0 | -|        `The smoothTangent Z component`| -|outLinearTarget (lt)| double3 | n/a | -|    `The target for next linear tangent`| -|    outLinearTargetX (ltx)| double | 0.0 | -|        `The linearTarget X component`| -|    outLinearTargetY (lty)| double | 0.0 | -|        `The linearTarget Y component`| -|    outLinearTargetZ (ltz)| double | 0.0 | -|        `The linearTarget Z component`| -| | | | -|parentInverseMatrix (pim)| matrix | Identity | -|    `The parent inverse matrix from the object connected to the output`| -|inTangent (it)| matrix | Identity | -|    `The user defined floating tangent matrix`| -|previousVertex (pv)| matrix | Identity | -|    `The CV that is on the opposite side of the current vertex`| -|currentVertex (cv)| matrix | Identity | -|    `The CV that this tangent is connect to`| -|nextVertex (nv)| matrix | Identity | -|    `The CV that this tangent points towards`| -|inLinearTarget (nlt)| double3 | n/a | -|    `Connect the outLinearTarget from another TwistTangent node controlling the same bezier segment here`| -|    inLinearTargetX (nltx)| double | 0.0 | -|        `The linearTarget X component`| -|    inLinearTargetY (nlty)| double | 0.0 | -|        `The linearTarget Y component`| -|    inLinearTargetZ (nltz)| double | 0.0 | -|        `The linearTarget Z component`| -|auto (a)| double | 1.0 | -|    `Whether or not the output is controlled automatically by the CVs`| -|smooth (s)| double | 1.0 | -|    `Whether an automatic output is smooth or linear`| -|weight (w)| double | 1.0 | -|    `The Length of the auto tangent. A weight of 1 is 1/3 of the distance between the current and next CVs`| - -## RiderConstraint Node - -A node that gets transformations at a given parameters along the spline. Generally, you will make many rider objects, and skin your geometry to those riders. - -This node contains many convenience options. Normalization allows easy 0 to 1 parameterization no matter the length of the spline (as a spline that is unpinned will default to parameterizing by length). The globalOffset and globalSpread parameters are common sliding options. And useCycle allows you to constrain in loops so any rider that goes past the end parameter will loop back to the beginning, and vice-versa. - -The multiple spline inputs allows you to switch between controlling splines. For instance, you could build a second spline with extra controls as the need arose, and swap control of the riders to that new spline. - -Note: Despite its name, this isn't actually implemented as a constraint, but it behaves similarly. - -| Long Name (short name) | Type | Default | -| --- | --- | --- | -|rotateOrder (ro) | enum | XYZ | -|    `Enum of the rotation order standard to Maya`| -|globalOffset (go) | double | 0.0 | -|    `A value added to all input parameters. This shifts everything connected to this constraint along the spline.`| -|globalSpread (gs) | double | 1.0 | -|    `A value multiplied by all input parameters. This spreads everything out (happens before the offset)`| -|useCycle (uc) | | false | -|    `Whether or not to cycle the parameters once they go past the end. If not, they extrapolate linearly.`| -|normalize (n) | boolen | True | -|    `If true, then the input parameters are remapped so that (0, normValue) maps to (0, restLength) of the spline`| -|normValue (nv) | double | 1.0 | -|    `The remapped maximum value when normalizing`| -|inputSplines (is) | compound | n/a | -|    `The group that is a spline and its corresponding weight.`| -|    spline (s) | -|        `An input spline`| -|    weight (w) | double | 1.0 | -|        `The constraint weight of that spline`| -|params (ps) | -|    `The parameters for the constraints, and their parent inverse matrices`| -|    param (p) | double | 0.0 | -|        `The parameter where an object will stick to the spline.`| -|    parentInverseMatrix (pim) | matrix | identity | -|        `The parentInverseMatrix of the object sticking to the spline`| -| | | -|outputs (out) | -|    translate (t) | double3 | n/a | -|        `The output translation`| -|        translateX (tx) | double | 0.0 | -|            `The output translation X Component`| -|        translateY (ty) | double | 0.0 | -|            `The output translation Y Component`| -|        translateZ (tz) | double | 0.0 | -|            `The output translation Z Component`| -|    rotate (rot) | double3 | n/a | -|        `The output rotation`| -|        rotateX (rotx) | angle | 0.0 | -|            `The output rotation X Component`| -|        rotateY (roty) | angle | 0.0 | -|            `The output rotation Y Component`| -|        rotateZ (rotz) | angle | 0.0 | -|            `The output rotation Z Component`| -|    scale (scl) | double3 | n/a | -|        `The output scale`| -|        scaleX (sclx) | double | 0.0 | -|            `The output scale X Component`| -|        scaleY (scly) | double | 0.0 | -|            `The output scale Y Component`| -|        scaleZ (sclz) | double | 0.0 | -|            `The output scale Z Component`| +## Node Documentation + +See the [Node Documentation](nodeDocs.md) file diff --git a/mayaConfigure.bat b/mayaConfigure.bat index d835254..204be91 100644 --- a/mayaConfigure.bat +++ b/mayaConfigure.bat @@ -5,6 +5,6 @@ SET BUILD=mayabuild_%MAYA_VERSION% SET COMPILER=Visual Studio 16 2019 cmake -B ./%BUILD% -DMAYA_VERSION="%MAYA_VERSION%" -G "%COMPILER%" -cmake --build ./%BUILD% --config Release +cmake --build ./%BUILD% --config RelWithDebInfo pause diff --git a/nodeDocs.md b/nodeDocs.md new file mode 100644 index 0000000..2a26723 --- /dev/null +++ b/nodeDocs.md @@ -0,0 +1,261 @@ +## TwistSpline Node + +This node implements the TwistSpline, which is a dynamically re-parameterizing bezier spline that interpolates twist along its length. + +This TwistSpline can have any number of Control Vertices, and each control vertex has an incoming and outgoing tangent. The incoming tangent is ignored for the first CV, and the outgoing tangent is ignored for the last CV. + +The OutputSpline plug is a custom type specific to this set of nodes. + +The per-CV plugs control how the spline behaves and reparameterizes. The ControlVertex plug recieves the worldpace position and orientation of the CV itself. The InTangent and OutTangent are world matrices that define the bezier tangent positions and orientations for this CV. ParamValue and ParamWeight are used as a pair. ParamValue is the parameter that the currentCV will have when ParamWeight is 1.0. If ParamWeight is 0.0, then the parameter of this CV will float between it's neighbor's parameters (as a percentage of the lengths of its neighboring bezier spline segments). The parameterization algorithm works as long as any paramWeight is greater than 0. + +TwistValue and TwistWeight work in a similar way. Twists are interpolated between values that have weight, and skip those that don't. Because this is handled as an interpolated float value, twist behavior allows for arbitrary values, and can twist past 360deg multiple times along the spline. + +UseOrient is the "weight" of the ControlVertex worldspace orientation as part of the twist. Because of the limitations of Matrices and Quaternions, you can never get more than a 180 degree twist using orient behavior and popping can occur if you're not careful. + +DebugDisplay and DebugScale show the orientation frames calculated along the spline and control their size. + +The twistMultipler plug exists for backwards compatibility with bug in earlier versions of the code. The old multi-node auto-tangent setup accidentally built a left-handed twist matrix, thus the twist values went the wrong way. The new single-node auto-tangent builds the correct matrices, and thus this multipler must be set to 1.0 in that case. + +ScaleCompensation exists because each input parameter would need a multipler attached to it to allow uniform scaling without changing behavior. This plug and the one on the rider handles all of that internally. + +| Long Name (short name) | Type | Default | +| --- | --- | --- | +|outputSpline (os) | TwistSpline | n/a | +|___`The custom spline data output` | | | +|outputNurbsCurve (onc) | NurbsCurve | n/a | +|___`A Maya-native NURBS curve output. Note: It will reparameterize by length, but it will not include twist data` | | | +|splineLength (sl) | double | 0.0 | +|___`The total length of the spline` | | | +|scaleCompensation (sclcmp) | double | 1.0 | +|___`The overall scale of the spline. This is required to make riding/pinning/offsets work correctly` | | | +|splineDisplay (sd) | bool | True | +|___`Whether to draw the spline` | | | +|debugDisplay (dd) | bool | False | +|___`Whether to show the debug axes` | | | +|debugScale (ds) | double | 1.0 | +|___`The size of the debug axes` | | | +|twistMultiplier (tm) | double | -1.0 | +|___`A multiplier on top of the intput TwistValue inputs. Exists for backwards compatibility` | | | +|vertexData (vd) | compound | n/a | +|___Inputs for all of the per-cv data | | | +|___inTangent (int) | matrix | Identity | +|______`The bezier tangent pointing towards the start of the spline` | | | +|___outTangent (ot) | matrix | Identity | +|______`The bezier tangent pointing towards the end of the spline` | | | +|___controlVertex (cv) | matrix | Identity | +|______`The control vertex` | | | +|___paramValue (pv) | double | 0.0 | +|______`The parameter value of this CV` | | | +|___paramWeight (pw) | double | 0.0 | +|______`Whether to use the paramValue, or float` | | | +|___twistValue (tv) | double | 0.0 | +|______`The twist value of this CV` | | | +|___twistWeight (tw) | double | 0.0 | +|______`Whether to use the twist value, or interpolate` | | | +|___useOrient (uo) | double | 0.0 | +|______`Whether to use the orientation of this CV as part of the twist` | | | + + +## TwistMultiTangent Node + +This node controls the behavior of all automatic bezier tangents for a control. + +| Long Name (short name) | Type | Default | +| --- | --- | --- | +| vertData (vd)| compound | n/a | +| ___ `The input vertex user data` | | | +| ___vertMat (vm) | matrix | Identity | +| ______ `The Control Vertex world matrix input` | | | +| ___inParentInverseMatrix (ipim) | matrix | Identity | +| ______ `The parentInverseMatrix of the inTangent` | | | +| ___outParentInverseMatrix (opim) | matrix | Identity | +| ______ `The parentInverseMatrix of the outTangent` | | | +| ___twistParentInverseMatrix (tpim) | matrix | Identity | +| ______ `The parentInverseMatrix of the twist axis` | | | +| ___inTanWeight (itw) | double | 1.0 | +| ______`The Length of the in auto tangent. A weight of 1 is 1/3 of the distance between the current and next CVs`| | | +| ___outTanWeight (otw) | double | 1.0 | +| ______`The Length of the out auto tangent. A weight of 1 is 1/3 of the distance between the current and next CVs`| | | +| ___inSmooth (ism) | double | 1.0 | +| ______ `Whether an automatic in tangent is smooth or linear` | | | +| ___outSmooth (osm) | double | 1.0 | +| ______ `Whether an automatic out tangent is smooth or linear` | | | +| ___inAuto (iat) | double | 1.0 | +| ______ `Whether or not the output is controlled automatically by the CVs` | | | +| ___outAuto (oat) | double | 1.0 | +| ______ `Whether or not the output is controlled automatically by the CVs` | | | +| ___inTanMat (itm) | matrix | Identity | +| ______ `The worldspace rest position of the inTangent` | | | +| ___outTanMat (otm) | matrix | Identity | +| ______ `The worldspace rest position of the outTangent` | | | +| startTension (st) | double | 2.0 | +| ___ `How much the first tangent overshoots` | | | +| endTension (et) | double | 2.0 | +| ___ `How much the last tangent overshoots` | | | +| maxVertices (mv) | Int | 999 | +| ___ `The maximum number of vertices that will be calculated` | | | +| closed (cl) | Bool | False | +| ___ `Whether or not the tangents behave as a closed loop` | | | +| vertTans (vt) | compound | +| ___ `The output tangent data` | | | +| ___inVertTan (ivt) | double3 | n/a | +| ______ `The output inTangent` | | | +| ______inVertTanX (ivx) | double | 0.0 | +| _________ `The output inTangent X component` | | | +| ______inVertTanY (ivy) | double | 0.0 | +| _________ `The output inTangent Y component` | | | +| ______inVertTanZ (ivz) | double | 0.0 | +| _________ `The output inTangent Z component` | | | +| ___outVertTan (ovt) | double3 | n/a | +| ______ `The output outTangent` | | | +| ______outVertTanX (ovx) | double | 0.0 | +| _________ `The output outTangent X component` | | | +| ______outVertTanY (ovy) | double | 0.0 | +| _________ `The output outTangent Y component` | | | +| ______outVertTanZ (ovz) | double | 0.0 | +| _________ `The output outTangent Z component` | | | +| ___inTanLen (itl) | double | 0.0 | +| ______ `The computed length of the inTangent` | | | +| ___outTanLen (otl) | double | 0.0 | +| ______ `The computed length of the outTangent` | | | +| ___twistUp (tu) | double3 | n/a | +| ______ `The direction of "up" for the twist axis` | | | +| ______twistUpX (tux) | double | 0.0 | +| _________ `The direction of "up" for the twist axis X component` | | | +| ______twistUpY (tuy) | double | 0.0 | +| _________ `The direction of "up" for the twist axis Y component` | | | +| ______twistUpZ (tuz) | double | 0.0 | +| _________ `The direction of "up" for the twist axis Z component` | | | +| ___twistMat (tm) | matrix | Identity | +| ______ `The matrix for the twist axis that will keep it aligned with the auto tangents` | | | + + +## TwistTangent Node + +### THIS NODE IS DEPRECATED. YOU SHOULD SWITCH TO THE SINGLE-NODE TANGENT SETUP + +This node Controls the behavior of a single bezier tangent control. + +This node assumes the tangent being controlled is an outgoing tangent. So for incoming tangents, the "previous" and "next" cvs are swapped. + +These nodes usually come in pairs. One for each twist spline segment. The in/out linear targets are connected in a cycle between them. Don't worry, it's not an *actual* cycle as the plugs aren't connected that way internally to the node. + +| Long Name (short name) | Type | Default | +| --- | --- | --- | +|out (out)| double3 | n/a | +|___`The final output from this node. Usually connected to a tangent input on a TwistSpline node`| +|___outX (ox)| double | 0.0 | +|______`The output X component`| +|___outY (oy)| double | 0.0 | +|______`The output Y component`| +|___outZ (oz)| double | 0.0 | +|______`The output Z component`| +|smoothTan (st)| double3 | n/a | +|___`The pure smooth tangent in un-oriented CV space. Not affected by weight or auto`| +|___smoothTanX (stx)| double | 0.0 | +|______`The smoothTangent X component`| +|___smoothTanY (sty)| double | 0.0 | +|______`The smoothTangent Y component`| +|___smoothTanZ (stz)| double | 0.0 | +|______`The smoothTangent Z component`| +|outLinearTarget (lt)| double3 | n/a | +|___`The target for next linear tangent`| +|___outLinearTargetX (ltx)| double | 0.0 | +|______`The linearTarget X component`| +|___outLinearTargetY (lty)| double | 0.0 | +|______`The linearTarget Y component`| +|___outLinearTargetZ (ltz)| double | 0.0 | +|______`The linearTarget Z component`| +| | | | +|parentInverseMatrix (pim)| matrix | Identity | +|___`The parent inverse matrix from the object connected to the output`| +|inTangent (it)| matrix | Identity | +|___`The user defined floating tangent matrix`| +|previousVertex (pv)| matrix | Identity | +|___`The CV that is on the opposite side of the current vertex`| +|currentVertex (cv)| matrix | Identity | +|___`The CV that this tangent is connect to`| +|nextVertex (nv)| matrix | Identity | +|___`The CV that this tangent points towards`| +|inLinearTarget (nlt)| double3 | n/a | +|___`Connect the outLinearTarget from another TwistTangent node controlling the same bezier segment here`| +|___inLinearTargetX (nltx)| double | 0.0 | +|______`The linearTarget X component`| +|___inLinearTargetY (nlty)| double | 0.0 | +|______`The linearTarget Y component`| +|___inLinearTargetZ (nltz)| double | 0.0 | +|______`The linearTarget Z component`| +|auto (a)| double | 1.0 | +|___`Whether or not the output is controlled automatically by the CVs`| +|smooth (s)| double | 1.0 | +|___`Whether an automatic output is smooth or linear`| +|weight (w)| double | 1.0 | +|___`The Length of the auto tangent. A weight of 1 is 1/3 of the distance between the current and next CVs`| + +## RiderConstraint Node + +A node that gets transformations at a given parameters along the spline. Generally, you will make many rider objects, and skin your geometry to those riders. + +This node contains many convenience options. Normalization allows easy 0 to 1 parameterization no matter the length of the spline (as a spline that is unpinned will default to parameterizing by length). The globalOffset and globalSpread parameters are common sliding options. And useCycle allows you to constrain in loops so any rider that goes past the end parameter will loop back to the beginning, and vice-versa. + +The multiple spline inputs allows you to switch between controlling splines. For instance, you could build a second spline with extra controls as the need arose, and swap control of the riders to that new spline. + +ScaleCompensation exists because each input parameter would need a multipler attached to it to allow uniform scaling without changing behavior. This plug and the one on the spline handles all of that internally. + +Note: Despite its name, this isn't actually implemented as a constraint, but it behaves similarly. + +| Long Name (short name) | Type | Default | +| --- | --- | --- | +|rotateOrder (ro) | enum | XYZ | +|___`Enum of the rotation order standard to Maya`| +|globalOffset (go) | double | 0.0 | +|___`A value added to all input parameters. This shifts everything connected to this constraint along the spline.`| +|globalSpread (gs) | double | 1.0 | +|___`A value multiplied by all input parameters. This spreads everything out (happens before the offset)`| +|scaleCompensation (sclcmp) | double | 1.0 | +|___`The overall scale of the spline. This is required to make riding/pinning/offsets work correctly` | | | +|useCycle (uc) | | false | +|___`Whether or not to cycle the parameters once they go past the end. If not, they extrapolate linearly.`| +|normalize (n) | boolen | True | +|___`If true, then the input parameters are remapped so that (0, normValue) maps to (0, restLength) of the spline`| +|normValue (nv) | double | 1.0 | +|___`The remapped maximum value when normalizing`| +|inputSplines (is) | compound | n/a | +|___`The group that is a spline and its corresponding weight.`| +|___spline (s) | +|______`An input spline`| +|___weight (w) | double | 1.0 | +|______`The constraint weight of that spline`| +|params (ps) | +|___`The parameters for the constraints, and their parent inverse matrices`| +|___param (p) | double | 0.0 | +|______`The parameter where an object will stick to the spline.`| +|___parentInverseMatrix (pim) | matrix | identity | +|______`The parentInverseMatrix of the object sticking to the spline`| +| | | +|outputs (out) | +|___translate (t) | double3 | n/a | +|______`The output translation`| +|______translateX (tx) | double | 0.0 | +|_________`The output translation X Component`| +|______translateY (ty) | double | 0.0 | +|_________`The output translation Y Component`| +|______translateZ (tz) | double | 0.0 | +|_________`The output translation Z Component`| +|___rotate (rot) | double3 | n/a | +|______`The output rotation`| +|______rotateX (rotx) | angle | 0.0 | +|_________`The output rotation X Component`| +|______rotateY (roty) | angle | 0.0 | +|_________`The output rotation Y Component`| +|______rotateZ (rotz) | angle | 0.0 | +|_________`The output rotation Z Component`| +|___scale (scl) | double3 | n/a | +|______`The output scale`| +|______scaleX (sclx) | double | 0.0 | +|_________`The output scale X Component`| +|______scaleY (scly) | double | 0.0 | +|_________`The output scale Y Component`| +|______scaleZ (sclz) | double | 0.0 | +|_________`The output scale Z Component`| + diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..96012c0 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,82 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + "*.egg-info", + "*.pyc", + ".bzr", + ".cache", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pycache__", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "docs", + "node_modules", + "shared-venv", + "site-packages", + "venv", +] + +line-length = 100 +indent-width = 4 +target-version = "py37" + +[lint] +select = [ + "B", + "C", + "E", + "F", + "N", + "W", + "B9", +] +ignore = [ + "B905", + "C901", + "E203", + "E501", + "E722", + "N802", + "N803", + "N804", + "N806", + "N815", +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" diff --git a/scripts/AEtwistMultiTangentTemplate.mel b/scripts/AEtwistMultiTangentTemplate.mel new file mode 100644 index 0000000..c29ab47 --- /dev/null +++ b/scripts/AEtwistMultiTangentTemplate.mel @@ -0,0 +1,16 @@ +// Attribute Editor Template for Blur Studio's 'Twist Spline' plugin. + +global proc AEtwistMultiTangentTemplate( string $nodeName ) { + editorTemplate -beginScrollLayout; + + editorTemplate -beginLayout "Spline Tangent Attributes" -collapse 0; + editorTemplate -label "Start Tension" -addControl "startTension"; + editorTemplate -label "End Tension" -addControl "endTension"; + editorTemplate -label "Max Vertices" -addControl "maxVertices"; + editorTemplate -label "Closed" -addControl "closed"; + editorTemplate -label "Vert Data" -addControl "vertData"; + editorTemplate -endLayout; + + editorTemplate -addExtraControls; + editorTemplate -endScrollLayout; +} diff --git a/scripts/twistSplineBuilder.py b/scripts/twistSplineBuilder.py index e364849..103405a 100644 --- a/scripts/twistSplineBuilder.py +++ b/scripts/twistSplineBuilder.py @@ -1,4 +1,4 @@ -''' +""" MIT License Copyright (c) 2018 Blur Studio @@ -20,7 +20,7 @@ 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. -''' +""" from maya import cmds, OpenMaya @@ -29,7 +29,7 @@ DFM_BFR_FMT = "Hbfr_X_{0}Rider_Part{1:02d}" # Rider Buffer DFM_FMT = "Dfm_X_{0}Rider_Part{1:02d}" # Deformer SPLINE_FMT = "Rig_X_{0}Spline_Drv" # Spline name -MASTER_FMT = "Ctrl_X_{}SplineGlobal_Part" # Global control +MASTER_FMT = "Ctrl_X_{0}SplineGlobal_Part" # Global control CTRL_ORG_FMT = "Org_X_{0}_Ctrls" # Control organizer BFR_CV_FMT = "Hbfr_X_{0}Spline_Part{1:02d}" # CV Buffer CTRL_CV_FMT = "Ctrl_X_{0}Spline_Part{1:02d}" # CV @@ -39,658 +39,947 @@ CTRL_OUTTAN_FMT = "Ctrl_X_{0}OutTangent_Part{1:02d}" # Out-Tangent BFR_AINTAN_FMT = "Hbfr_X_{0}AutoInTangent_Part{1:02d}" # Auto In-Tangent Buffer BFR_AOUTTAN_FMT = "Hbfr_X_{0}AutoOutTangent_Part{1:02d}" # Auto Out-Tangent Buffer +BFR_AINREST_FMT = "Hbfr_X_{0}RestInTangent_Part{1:02d}" # Auto In-Tangent Rest Buffer +BFR_AOUTREST_FMT = "Hbfr_X_{0}RestOutTangent_Part{1:02d}" # Auto Out-Tangent Rest Buffer + def makeLinkLine(sourceNode, destNode, selectNode=None): - """ Draw a line between two nodes. Clicking the line selects the target object + """Draw a line between two nodes. Clicking the line selects the target object + + Arguments: + sourceNode (str): The start of the line + destNode (str): The end of the line + selectNode (str): The object that gets selected. Defaults to sourceNode - Arguments: - sourceNode (str): The start of the line - destNode (str): The end of the line - selectNode (str): The object that gets selected. Defaults to sourceNode + Returns: + str: The line's shape node - Returns: - str: The line's shape node + """ + if selectNode is None: + selectNode = sourceNode - """ - if selectNode is None: - selectNode = sourceNode + lineTfm = cmds.curve(degree=1, point=([0, 0, 0], [0, 0, 1]), knot=(0, 1)) + lineShp = cmds.listRelatives(lineTfm, shapes=True, path=True)[0] + cmds.parent(lineShp, selectNode, relative=True, shape=True, noConnections=True) + cmds.delete(lineTfm) - lineTfm = cmds.curve(d=1, p=([0, 0, 0], [0, 0, 1]), k=(0, 1)) - lineShape = cmds.listRelatives(lineTfm, s=True, path=True)[0] - cmds.parent(lineShape, selectNode, r=True, s=True, nc=True) - cmds.delete(lineTfm) + for idx, node in enumerate([destNode, sourceNode]): + if node == selectNode: + # If so, we can skip all the connections and just set the control point + # to the local rotPivot, and leave it there + rotPivot = cmds.xform(sourceNode, query=True, objectSpace=True, rotatePivot=True) + cmds.setAttr(f"{lineShp}.controlPoints[{idx}]", *rotPivot) + else: + wmat = cmds.createNode("pointMatrixMult", name=f"{node}_linkCurveWorldMat") + iwmat = cmds.createNode("pointMatrixMult", name=f"{selectNode}_linkCurveWorldMat") + rotPivot = cmds.xform(node, query=True, objectSpace=True, rotatePivot=True) - for idx, node in enumerate([destNode, sourceNode]): - if node == selectNode: - # If so, we can skip all the connections and just set the control point - # to the local rotPivot, and leave it there - rotPivot = cmds.xform(sourceNode, q=True, objectSpace=True, rotatePivot=True) - cmds.setAttr(lineShape + ".controlPoints[{0}]".format(idx), *rotPivot) - else: - worldMatrix = cmds.createNode('pointMatrixMult', name=node + "_linkCurveWorldMat") - inverseMatrix = cmds.createNode('pointMatrixMult', name=selectNode + "_linkCurveWorldMat") - rotPivot = cmds.xform(node, q=True, objectSpace=True, rotatePivot=True) + cmds.connectAttr(f"{node}.worldMatrix", f"{wmat}.inMatrix", force=True) + cmds.setAttr(f"{wmat}.inPoint", *rotPivot) + cmds.connectAttr(f"{selectNode}.worldInverseMatrix", f"{iwmat}.inMatrix", force=True) - cmds.connectAttr(node + ".worldMatrix", worldMatrix + ".inMatrix", f=True) - cmds.setAttr(worldMatrix + '.inPoint', *rotPivot) - cmds.connectAttr(selectNode + ".worldInverseMatrix", inverseMatrix + ".inMatrix", f=True) + cmds.connectAttr(f"{wmat}.output", f"{iwmat}.inPoint", force=True) + cmds.connectAttr( + f"{iwmat}.output", + f"{lineShp}.controlPoints[{idx}]", + force=True, + ) - cmds.connectAttr(worldMatrix + ".output", inverseMatrix + ".inPoint", f=True) - cmds.connectAttr(inverseMatrix + ".output", lineShape + ".controlPoints[{0}]".format(idx), f=True) + for node in [wmat, iwmat]: + cmds.setAttr(f"{node}.isHistoricallyInteresting", False) - cmds.setAttr(lineShape + ".overrideEnabled", 1) - cmds.setAttr(lineShape + ".overrideColor", 3) - cmds.rename(lineShape, sourceNode + "Shape") + cmds.setAttr(f"{lineShp}.overrideEnabled", 1) + cmds.setAttr(f"{lineShp}.overrideColor", 3) + cmds.rename(lineShp, f"{sourceNode}Shape") - for node in [worldMatrix, inverseMatrix]: - cmds.setAttr(node + ".isHistoricallyInteresting", False) + return lineShp - return lineShape def _mkMasterHarbieControllers(scale=1.0): - """ Make the master objects so we can duplicate them to be the controllers - This function uses the internal blur locators - - Returns: - cvCtrl: The CV controller master - outTanCtrl: The out-tangent controller master - inTanCtrl: The in-tangent controller master - twistCtrl: The twist controller master - masterCtrl: The top controller master - """ - # Blur Specific locators - from dcc.maya.cast import toPath - from dcc.maya.icon import createHarbieLocator as cloc - - cvCtrl = toPath(cloc("Cube", "TMP_SplineCV", None, None, size=1.0*scale, color=[1, 0.6, 0])) - oTanCtrl = toPath(cloc("Cross", "TMP_OTan", None, None, size=0.4*scale, ro=[0, 0, 0], color=[1, 0.6, 0.6])) - iTanCtrl = toPath(cloc("Square", "TMP_ITan", None, None, size=0.4*scale, ro=[90, 0, 0], color=[0, 1, 1])) - twistCtrl = toPath(cloc("Compass", "TMP_SplineTwist", None, None, size=1.5*scale, ro=[-90, 90, 0], color=[0.5, 0, 1])) - masterCtrl = toPath(cloc("CrossArrow", "TMP_SplineGlobal", None, None, size=3.0*scale, ro=[90, 0, 0], color=[0, 1, 0])) - return cvCtrl, oTanCtrl, iTanCtrl, twistCtrl, masterCtrl + """Make the master objects so we can duplicate them to be the controllers + This function uses the internal blur locators + + Returns: + cvCtrl: The CV controller master + outTanCtrl: The out-tangent controller master + inTanCtrl: The in-tangent controller master + twistCtrl: The twist controller master + masterCtrl: The top controller master + """ + # Blur Specific locators + from dcc.maya.cast import toPath + from dcc.maya.icon import createHarbieLocator as cloc + + cvCtrl = toPath(cloc("Cube", "TMP_SplineCV", None, None, size=1.0 * scale, color=[1, 0.6, 0])) + oTanCtrl = toPath( + cloc( + "Cross", + "TMP_OTan", + None, + None, + size=0.4 * scale, + ro=[0, 0, 0], + color=[1, 0.6, 0.6], + ) + ) + iTanCtrl = toPath( + cloc( + "Square", + "TMP_ITan", + None, + None, + size=0.4 * scale, + ro=[90, 0, 0], + color=[0, 1, 1], + ) + ) + twistCtrl = toPath( + cloc( + "Compass", + "TMP_SplineTwist", + None, + None, + size=1.5 * scale, + ro=[-90, 90, 0], + color=[0.5, 0, 1], + ) + ) + masterCtrl = toPath( + cloc( + "CrossArrow", + "TMP_SplineGlobal", + None, + None, + size=3.0 * scale, + ro=[90, 0, 0], + color=[0, 1, 0], + ) + ) + return cvCtrl, oTanCtrl, iTanCtrl, twistCtrl, masterCtrl + def _mkMasterControllers(scale=1.0): - """ Make the master objects so we can duplicate them to be the controllers - - Returns: - cvCtrl: The CV controller master - outTanCtrl: The out-tangent controller master - inTanCtrl: The in-tangent controller master - twistCtrl: The twist controller master - masterCtrl: The top controller master - """ - v = 0.5 * scale - cvCtrl = cmds.curve( - degree=1, - p=[ - # top loop - (v, v, v), (v, -v, v), (-v, -v, v), (-v, v, v), (v, v, v), - # go to the bottom layer - (v, v, -v), - # One side of the bottom, and a vertical leg *3 - (v, -v, -v), (v, -v, v), (v, -v, -v), - (-v, -v, -v), (-v, -v, v), (-v, -v, -v), - (-v, v, -v), (-v, v, v), (-v, v, -v), - # Close the bottom - (v, v, -v), - ]) - - v = 0.25 * scale - outTanCtrl = cmds.circle(radius=v, constructionHistory=False)[0] - - v = 0.25 * scale - inTanCtrl = cmds.curve(degree=1, p=[(v, v, 0), (v, -v, 0), (-v, -v, 0), (-v, v, 0), (v, v, 0)]) - - v = 0.5 * scale - s = 0.5 * scale - twistCtrl = cmds.curve(degree=1, p=[(-v, s, 0), (v, s, 0), (0, 2 * v + s, 0), (-v, s, 0)]) - - v = 0.15 * scale - s = 1.108 * scale - o = 3 * scale - masterCtrl = cmds.curve( - degree=3, - periodic=True, - p=[ - (v, -v + o, 0), (0, -s + o, 0), (-v, -v + o, 0), (-s, 0 + o, 0), - (-v, v + o, 0), (0, s + o, 0), (v, v + o, 0), (s, 0 + o, 0), - (v, -v + o, 0), (0, -s + o, 0), (-v, -v + o, 0) - ], - k=[-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) - return cvCtrl, outTanCtrl, inTanCtrl, twistCtrl, masterCtrl - -def mkTwistSplineControllers(pfx, numCVs, spread, closed=False): - """ Make and position all the controller objects - - Arguments: - pfx (str): The user name of the spline. Will be formatted into the given naming convention - numCVs (int): The number of CVs to create for a spline - spread (float): The distance between each controller (including tangents) - closed (bool): Whether the spline forms a closed loop - - Returns: - [str, ...]: All the CV's - [str, ...]: All the CV Buffers - [str, ...]: All the Out-Tangents - [str, ...]: All the In-Tangents - [str, ...]: All the Auto-Out-Tangents - [str, ...]: All the Auto-In-Tangents - [str, ...]: All the Twisters - [str, ...]: All the Twister Buffers - str: The base controller - """ - - # Make bases for the controllers - cvCtrl, outTanCtrl, inTanCtrl, twistCtrl, masterCtrl = _mkMasterControllers() - - master = cmds.duplicate(masterCtrl, name=MASTER_FMT.format(pfx))[0] - cmds.addAttr(master, longName="Offset", attributeType='double', defaultValue=0.0) - cmds.setAttr(master + '.Offset', edit=True, keyable=True) - cmds.addAttr(master, longName="Stretch", attributeType='double', defaultValue=1.0, minValue=0.0001) - cmds.setAttr(master + '.Stretch', edit=True, keyable=True) - - # make the requested number of CV's - # don't hide the .rx attribute - cvs, tws, cvBfrs, twBfrs = [], [], [], [] - controlsGrp = cmds.createNode("transform", name=CTRL_ORG_FMT.format(pfx)) - cmds.parentConstraint(master, controlsGrp, mo=True) - cmds.scaleConstraint(master, controlsGrp, mo=True) - - for i in range(numCVs): - cvBfr = cmds.createNode("transform", name=BFR_CV_FMT.format(pfx, i + 1), parent=controlsGrp) - cv = cmds.duplicate(cvCtrl, name=CTRL_CV_FMT.format(pfx, i + 1))[0] - twBfr = cmds.createNode("transform", name=BFR_TWIST_FMT.format(pfx, i + 1), parent=controlsGrp) - tw = cmds.duplicate(twistCtrl, name=CTRL_TWIST_FMT.format(pfx, i + 1))[0] - - cmds.addAttr(cv, longName="Pin", attributeType='double', defaultValue=0.0, minValue=0.0, maxValue=1.0) - cmds.setAttr(cv + '.Pin', edit=True, keyable=True) - cmds.addAttr(cv, longName="PinParam", attributeType='double', defaultValue=0.0, minValue=0.0) - cmds.setAttr(cv + '.PinParam', edit=True, keyable=True) - - for h in ['.tx', '.ty', '.tz', '.ry', '.rz', '.sx', '.sy', '.sz', '.v']: - cmds.setAttr(tw + h, lock=True, keyable=False, channelBox=False) - cmds.addAttr(tw, longName="UseTwist", attributeType='double', defaultValue=0.0, minValue=0.0, maxValue=1.0) - cmds.setAttr(tw + '.UseTwist', edit=True, keyable=True) - cv, = cmds.parent(cv, cvBfr) - twBfr, = cmds.parent(twBfr, cv) - tw, = cmds.parent(tw, twBfr) - cmds.xform(cvBfr, translation=(spread * 3 * i, 0, 0)) - cvs.append(cv) - tws.append(tw) - cvBfrs.append(cvBfr) - twBfrs.append(twBfr) - - # make the tangents and auto-tangents - oTans, iTans, aoTans, aiTans = [], [], [], [] - - segments = numCVs if closed else numCVs - 1 - for i in range(segments): - # make oTan, and iTan - otNum = i - itNum = (i + 1) % numCVs - - oTan = cmds.duplicate(outTanCtrl, name=CTRL_OUTTAN_FMT.format(pfx, otNum + 1))[0] - iTan = cmds.duplicate(inTanCtrl, name=CTRL_INTAN_FMT.format(pfx, itNum + 1))[0] - for ndTan in [oTan, iTan]: - cmds.addAttr(ndTan, longName="Auto", attributeType='double', defaultValue=1.0, minValue=0.0, maxValue=1.0) - cmds.setAttr(ndTan + '.Auto', edit=True, keyable=True) - cmds.addAttr(ndTan, longName="Smooth", attributeType='double', defaultValue=1.0, minValue=0.0, maxValue=1.0) - cmds.setAttr(ndTan + '.Smooth', edit=True, keyable=True) - cmds.addAttr(ndTan, longName="Weight", attributeType='double', defaultValue=1.0, minValue=0.0001, maxValue=5.0) - cmds.setAttr(ndTan + '.Weight', edit=True, keyable=True) - - cmds.xform(oTan, translation=(spread * (3 * otNum + 1), 0, 0)) - cmds.xform(iTan, translation=(spread * (3 * itNum - 1), 0, 0)) - - aoTan = cmds.createNode("transform", name=BFR_AOUTTAN_FMT.format(pfx, otNum + 1), parent=cvs[otNum]) - aiTan = cmds.createNode("transform", name=BFR_AINTAN_FMT.format(pfx, itNum + 1), parent=cvs[itNum]) - - cmds.xform(aoTan, translation=(spread * (3 * otNum + 1), 0, 0)) - cmds.xform(aiTan, translation=(spread * (3 * itNum - 1), 0, 0)) - oTan = cmds.parent(oTan, cvs[otNum])[0] - iTan = cmds.parent(iTan, cvs[itNum])[0] - - oTans.append(oTan) - iTans.append(iTan) - aoTans.append(aoTan) - aiTans.append(aiTan) - - for nd in [aiTan, aoTan]: - cmds.setAttr(nd + ".overrideEnabled", 1) - cmds.setAttr(nd + ".overrideDisplayType", 2) - cmds.setAttr(nd + ".visibility", 0) - - makeLinkLine(aoTan, cvs[otNum], selectNode=oTan) - makeLinkLine(aiTan, cvs[itNum], selectNode=iTan) - - cmds.delete((cvCtrl, outTanCtrl, inTanCtrl, twistCtrl, masterCtrl)) - return cvs, cvBfrs, oTans, iTans, aoTans, aiTans, tws, twBfrs, master - - - - -def createTwistSetup(pre, cur, post, buf, isFirst=False, isLast=False): - """ Create an auto-twist setup for a set of CVs - - Arguments: - pre (str): The previous CV for auto-tangent calculations - cur (str): The CV that will have its auto-twist connected - post (str): The next CV for auto-tangent calculations - buf (str): The Buffer that will have its auto-twist connected - isFirst (bool): Whether this segment is the first one in the spline - isLast (bool): Whether this segment is the last one in the spline - """ - twt = cmds.createNode("twistTangent", name="twistAuto") - dcm = cmds.createNode("decomposeMatrix", name="twistDecompose") - - if isLast: - cmds.setAttr("{}.backpoint".format(twt), True) - if isFirst or isLast: - cmds.setAttr("{}.endpoint".format(twt), True) - - cmds.connectAttr("{}.worldMatrix[0]".format(pre), "{}.previousVertex".format(twt)) - cmds.connectAttr("{}.worldMatrix[0]".format(cur), "{}.currentVertex".format(twt)) - cmds.connectAttr("{}.worldMatrix[0]".format(post), "{}.nextVertex".format(twt)) - - cmds.connectAttr("{}.parentInverseMatrix[0]".format(buf), "{}.parentInverseMatrix".format(twt)) - cmds.connectAttr("{}.outTwistMat".format(twt), "{}.inputMatrix".format(dcm)) - cmds.connectAttr("{}.outputRotate".format(dcm), "{}.rotate".format(buf)) - cmds.connectAttr("{}.outputScale".format(dcm), "{}.scale".format(buf)) - cmds.connectAttr("{}.outputTranslate".format(dcm), "{}.translate".format(buf)) - - - - -def createTangentSegmentSetup(pre, start, end, nxt, oTan, iTan, aoTan, aiTan, isFirst=False, isLast=False): - """ Create a single twist spline tangent setup for a single segment - - With 4 CV's given, there are 3 segments between them. - This creates a tangent setup for the middle segment. - - Arguments: - pre (str): The previous CV for auto-tangent calculations - start (str): The CV that will have its out-tangent connected - end (str): The CV that will have its in-tangent connected - nxt (str): The last CV for auto-tangent calculations - oTan (str): The Out-Tangent object - iTan (str): The In-Tangent object - aoTan (str): The Auto-Out-Tangent object - aiTan (str): The Auto-In-Tangent object - isFirst (bool): Whether this segment is the first one in the spline - isLast (bool): Whether this segment is the last one in the spline - """ - # connect all the in/out tangents - ott = cmds.createNode("twistTangent", name="twistTangentOut") # TODO: make with name based on oTan - cmds.connectAttr("{}.worldMatrix[0]".format(oTan), "{}.inTangent".format(ott)) - cmds.connectAttr("{}.Auto".format(oTan), "{}.auto".format(ott)) - cmds.connectAttr("{}.Smooth".format(oTan), "{}.smooth".format(ott)) - cmds.connectAttr("{}.Weight".format(oTan), "{}.weight".format(ott)) - if not isFirst: - cmds.connectAttr("{}.worldMatrix[0]".format(pre), "{}.previousVertex".format(ott)) - else: - cmds.setAttr("{}.Smooth".format(oTan), 0.0) - - cmds.connectAttr("{}.worldMatrix[0]".format(start), "{}.currentVertex".format(ott)) - cmds.connectAttr("{}.worldMatrix[0]".format(end), "{}.nextVertex".format(ott)) - cmds.setAttr("{}.Auto".format(oTan), 1.0) - - itt = cmds.createNode("twistTangent", name="twistTangentIn") # TODO: make with name based on iTan - cmds.connectAttr("{}.worldMatrix[0]".format(iTan), "{}.inTangent".format(itt)) - cmds.connectAttr("{}.Auto".format(iTan), "{}.auto".format(itt)) - cmds.connectAttr("{}.Smooth".format(iTan), "{}.smooth".format(itt)) - cmds.connectAttr("{}.Weight".format(iTan), "{}.weight".format(itt)) - if not isLast: - cmds.connectAttr("{}.worldMatrix[0]".format(nxt), "{}.previousVertex".format(itt)) - else: - cmds.setAttr("{}.Smooth".format(iTan), 0.0) - - cmds.connectAttr("{}.worldMatrix[0]".format(end), "{}.currentVertex".format(itt)) - cmds.connectAttr("{}.worldMatrix[0]".format(start), "{}.nextVertex".format(itt)) - cmds.setAttr("{}.Auto".format(iTan), 1.0) - - cmds.connectAttr("{}.outLinearTarget".format(itt), "{}.inLinearTarget".format(ott)) - cmds.connectAttr("{}.outLinearTarget".format(ott), "{}.inLinearTarget".format(itt)) - - cmds.connectAttr("{0}.out".format(ott), "{}.translate".format(aoTan)) - cmds.connectAttr("{}.parentInverseMatrix[0]".format(aoTan), "{}.parentInverseMatrix".format(ott)) - - cmds.connectAttr("{0}.out".format(itt), "{}.translate".format(aiTan)) - cmds.connectAttr("{}.parentInverseMatrix[0]".format(aiTan), "{}.parentInverseMatrix".format(itt)) + """Make the master objects so we can duplicate them to be the controllers + + Returns: + cvCtrl: The CV controller master + outTanCtrl: The out-tangent controller master + inTanCtrl: The in-tangent controller master + twistCtrl: The twist controller master + masterCtrl: The top controller master + """ + v = 0.5 * scale + cvCtrl = cmds.curve( + degree=1, + point=[ + # top loop + (v, v, v), + (v, -v, v), + (-v, -v, v), + (-v, v, v), + (v, v, v), + # go to the bottom layer + (v, v, -v), + # One side of the bottom, and a vertical leg *3 + (v, -v, -v), + (v, -v, v), + (v, -v, -v), + (-v, -v, -v), + (-v, -v, v), + (-v, -v, -v), + (-v, v, -v), + (-v, v, v), + (-v, v, -v), + # Close the bottom + (v, v, -v), + ], + ) + + v = 0.25 * scale + outTanCtrl = cmds.circle(radius=v, constructionHistory=False)[0] + + v = 0.25 * scale + inTanCtrl = cmds.curve( + degree=1, point=[(v, v, 0), (v, -v, 0), (-v, -v, 0), (-v, v, 0), (v, v, 0)] + ) + + v = 0.5 * scale + s = 0.5 * scale + twistCtrl = cmds.curve(degree=1, point=[(-v, s, 0), (v, s, 0), (0, 2 * v + s, 0), (-v, s, 0)]) + + v = 0.15 * scale + s = 1.108 * scale + o = 3 * scale + masterCtrl = cmds.curve( + degree=3, + periodic=True, + point=[ + (v, -v + o, 0), + (0, -s + o, 0), + (-v, -v + o, 0), + (-s, 0 + o, 0), + (-v, v + o, 0), + (0, s + o, 0), + (v, v + o, 0), + (s, 0 + o, 0), + (v, -v + o, 0), + (0, -s + o, 0), + (-v, -v + o, 0), + ], + knot=[-2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + ) + return cvCtrl, outTanCtrl, inTanCtrl, twistCtrl, masterCtrl + + +def _addTangentAttrs(tan): + cmds.addAttr( + tan, + longName="Auto", + attributeType="double", + defaultValue=1.0, + minValue=0.0, + maxValue=1.0, + ) + cmds.setAttr(f"{tan}.Auto", edit=True, keyable=True) + cmds.addAttr( + tan, + longName="Smooth", + attributeType="double", + defaultValue=1.0, + minValue=0.0, + maxValue=1.0, + ) + cmds.setAttr(f"{tan}.Smooth", edit=True, keyable=True) + cmds.addAttr( + tan, + longName="Weight", + attributeType="double", + defaultValue=1.0, + minValue=0.0001, + maxValue=5.0, + ) + cmds.setAttr(f"{tan}.Weight", edit=True, keyable=True) + + +def _addCVAttrs(cv): + cmds.addAttr( + cv, + longName="Pin", + attributeType="double", + defaultValue=0.0, + minValue=0.0, + maxValue=1.0, + ) + cmds.setAttr(f"{cv}.Pin", edit=True, keyable=True) + cmds.addAttr( + cv, + longName="PinParam", + attributeType="double", + defaultValue=0.0, + minValue=0.0, + ) + cmds.setAttr(f"{cv}.PinParam", edit=True, keyable=True) + + +def _addTwistAttrs(tw): + for h in [".tx", ".ty", ".tz", ".ry", ".rz", ".sx", ".sy", ".sz", ".v"]: + cmds.setAttr(tw + h, lock=True, keyable=False, channelBox=False) + cmds.addAttr( + tw, + longName="UseTwist", + attributeType="double", + defaultValue=0.0, + minValue=0.0, + maxValue=1.0, + ) + cmds.setAttr(f"{tw}.UseTwist", edit=True, keyable=True) + + +def _makeMaster(masterCtrl, pfx): + master = cmds.duplicate(masterCtrl, name=MASTER_FMT.format(pfx))[0] + cmds.addAttr(master, longName="Offset", attributeType="double", defaultValue=0.0) + cmds.setAttr(f"{master}.Offset", edit=True, keyable=True) + cmds.addAttr( + master, + longName="Stretch", + attributeType="double", + defaultValue=1.0, + minValue=0.0001, + ) + cmds.setAttr(f"{master}.Stretch", edit=True, keyable=True) + return master + + +def _makeCVs(numCVs, spread, master, pfx, cvCtrl, twistCtrl): + cvs, tws, cvBfrs, twBfrs = [], [], [], [] + controlsGrp = cmds.createNode("transform", name=CTRL_ORG_FMT.format(pfx)) + cmds.parentConstraint(master, controlsGrp, maintainOffset=True) + cmds.scaleConstraint(master, controlsGrp, maintainOffset=True) + + for i in range(numCVs): + cvBfr = cmds.createNode("transform", name=BFR_CV_FMT.format(pfx, i + 1), parent=controlsGrp) + cv = cmds.duplicate(cvCtrl, name=CTRL_CV_FMT.format(pfx, i + 1))[0] + twName = BFR_TWIST_FMT.format(pfx, i + 1) + twBfr = cmds.createNode("transform", name=twName, parent=controlsGrp) + tw = cmds.duplicate(twistCtrl, name=CTRL_TWIST_FMT.format(pfx, i + 1))[0] + _addCVAttrs(cv) + _addTwistAttrs(tw) + + (cv,) = cmds.parent(cv, cvBfr) + (twBfr,) = cmds.parent(twBfr, cv) + (tw,) = cmds.parent(tw, twBfr) + cmds.xform(cvBfr, translation=(spread * 3 * i, 0, 0)) + cvs.append(cv) + tws.append(tw) + cvBfrs.append(cvBfr) + twBfrs.append(twBfr) + return cvs, tws, cvBfrs, twBfrs + + +def _makeTanCtrls(numCVs, closed, inTanCtrl, outTanCtrl, pfx, cvs, spread): + """make the tangents and auto-tangents""" + oTans, iTans, aoTans, aiTans = [], [], [], [] + segments = numCVs if closed else numCVs - 1 + for i in range(segments): + # make oTan, and iTan + otNum = i + itNum = (i + 1) % numCVs + + oTan = cmds.duplicate(outTanCtrl, name=CTRL_OUTTAN_FMT.format(pfx, otNum + 1))[0] + iTan = cmds.duplicate(inTanCtrl, name=CTRL_INTAN_FMT.format(pfx, itNum + 1))[0] + + _addTangentAttrs(iTan) + _addTangentAttrs(oTan) + + aoTanName = BFR_AOUTTAN_FMT.format(pfx, otNum + 1) + aiTanName = BFR_AINTAN_FMT.format(pfx, itNum + 1) + aoTan = cmds.createNode("transform", name=aoTanName, parent=cvs[otNum]) + aiTan = cmds.createNode("transform", name=aiTanName, parent=cvs[itNum]) + + oTan = cmds.parent(oTan, cvs[otNum])[0] + iTan = cmds.parent(iTan, cvs[itNum])[0] + + cmds.xform(oTan, translation=(spread, 0, 0)) + cmds.xform(iTan, translation=(-spread, 0, 0)) + cmds.xform(aoTan, translation=(spread, 0, 0)) + cmds.xform(aiTan, translation=(-spread, 0, 0)) + + oTans.append(oTan) + iTans.append(iTan) + aoTans.append(aoTan) + aiTans.append(aiTan) + + for nd in [aiTan, aoTan]: + cmds.setAttr(f"{nd}.overrideEnabled", 1) + cmds.setAttr(f"{nd}.overrideDisplayType", 2) + cmds.setAttr(f"{nd}.visibility", 0) + + makeLinkLine(aoTan, cvs[otNum], selectNode=oTan) + makeLinkLine(aiTan, cvs[itNum], selectNode=iTan) + + return oTans, iTans, aoTans, aiTans + + +def _makeSingleNodeTanCtrls(numCVs, closed, inTanCtrl, outTanCtrl, pfx, cvs, spread): + """make the tangents and auto-tangents""" + oCtrls, iCtrls, oBfrs, iBfrs, oRests, iRests = [], [], [], [], [], [] + + segments = numCVs if closed else numCVs - 1 + for i in range(segments): + # make oTan, and iTan + otNum = i + itNum = (i + 1) % numCVs + + oRest = cmds.createNode("transform", name=BFR_AOUTREST_FMT.format(pfx, otNum + 1)) + iRest = cmds.createNode("transform", name=BFR_AINREST_FMT.format(pfx, itNum + 1)) + oBfr = cmds.createNode("transform", name=BFR_AOUTTAN_FMT.format(pfx, otNum + 1)) + iBfr = cmds.createNode("transform", name=BFR_AINTAN_FMT.format(pfx, itNum + 1)) + oCtrl = cmds.duplicate(outTanCtrl, name=CTRL_OUTTAN_FMT.format(pfx, otNum + 1))[0] + iCtrl = cmds.duplicate(inTanCtrl, name=CTRL_INTAN_FMT.format(pfx, itNum + 1))[0] + _addTangentAttrs(oCtrl) + _addTangentAttrs(iCtrl) + cmds.parent(oCtrl, oBfr) + cmds.parent(iCtrl, iBfr) + cmds.parent(oBfr, oRest) + cmds.parent(iBfr, iRest) + cmds.parent(oRest, cvs[otNum]) + cmds.parent(iRest, cvs[itNum]) + cmds.xform(oRest, translation=(spread, 0, 0)) + cmds.xform(iRest, translation=(-spread, 0, 0)) + + oCtrls.append(oCtrl) + iCtrls.append(iCtrl) + oBfrs.append(oBfr) + iBfrs.append(iBfr) + oRests.append(oRest) + iRests.append(iRest) + + makeLinkLine(oCtrl, cvs[otNum], selectNode=oCtrl) + makeLinkLine(iCtrl, cvs[itNum], selectNode=iCtrl) + + return oCtrls, iCtrls, oBfrs, iBfrs, oRests, iRests + + +def mkTwistSplineControllers(pfx, numCVs, spread, closed=False, singleTangentNode=True): + """Make and position all the controller objects + + Arguments: + pfx (str): The user name of the spline. Will be formatted into the given naming convention + numCVs (int): The number of CVs to create for a spline + spread (float): The distance between each controller (including tangents) + closed (bool): Whether the spline forms a closed loop + + Returns: + [str, ...]: All the CV's + [str, ...]: All the CV Buffers + [str, ...]: All the Out-Tangents + [str, ...]: All the In-Tangents + [str, ...]: All the Auto-Out-Tangents + [str, ...]: All the Auto-In-Tangents + [str, ...]: All the Twisters + [str, ...]: All the Twister Buffers + str: The base controller + """ + # Make bases for the controllers + cvCtrl, outTanCtrl, inTanCtrl, twistCtrl, masterCtrl = _mkMasterControllers() + + master = _makeMaster(masterCtrl, pfx) + cvs, tws, cvBfrs, twBfrs = _makeCVs(numCVs, spread, master, pfx, cvCtrl, twistCtrl) + if singleTangentNode: + oCtrls, iCtrls, oBfrs, iBfrs, oRests, iRests = _makeSingleNodeTanCtrls( + numCVs, closed, inTanCtrl, outTanCtrl, pfx, cvs, spread + ) + else: + oRests, iRests = None, None + oCtrls, iCtrls, oBfrs, iBfrs = _makeTanCtrls( + numCVs, closed, inTanCtrl, outTanCtrl, pfx, cvs, spread + ) + + cmds.delete((cvCtrl, outTanCtrl, inTanCtrl, twistCtrl, masterCtrl)) + return cvs, cvBfrs, oBfrs, iBfrs, oRests, iRests, oCtrls, iCtrls, tws, twBfrs, master + + +def _createTangentSegmentSetup( + pre, start, end, nxt, oTan, iTan, aoTan, aiTan, isFirst=False, isLast=False +): + """Create a single twist spline tangent setup for a single segment + + With 4 CV's given, there are 3 segments between them. + This creates a tangent setup for the middle segment. + + Arguments: + pre (str): The previous CV for auto-tangent calculations + start (str): The CV that will have its out-tangent connected + end (str): The CV that will have its in-tangent connected + nxt (str): The last CV for auto-tangent calculations + oTan (str): The Out-Tangent object + iTan (str): The In-Tangent object + aoTan (str): The Auto-Out-Tangent object + aiTan (str): The Auto-In-Tangent object + isFirst (bool): Whether this segment is the first one in the spline + isLast (bool): Whether this segment is the last one in the spline + """ + # connect all the in/out tangents + # TODO: make with name based on oTan + ott = cmds.createNode("twistTangent", name="twistTangentOut") + + cmds.connectAttr(f"{oTan}.worldMatrix[0]", f"{ott}.inTangent") + cmds.connectAttr(f"{oTan}.Auto", f"{ott}.auto") + cmds.connectAttr(f"{oTan}.Smooth", f"{ott}.smooth") + cmds.connectAttr(f"{oTan}.Weight", f"{ott}.weight") + if not isFirst: + cmds.connectAttr(f"{pre}.worldMatrix[0]", f"{ott}.previousVertex") + else: + cmds.setAttr(f"{oTan}.Smooth", 0.0) + + cmds.connectAttr(f"{start}.worldMatrix[0]", f"{ott}.currentVertex") + cmds.connectAttr(f"{end}.worldMatrix[0]", f"{ott}.nextVertex") + cmds.setAttr(f"{oTan}.Auto", 1.0) + + # TODO: make with name based on iTan + itt = cmds.createNode("twistTangent", name="twistTangentIn") + cmds.connectAttr(f"{iTan}.worldMatrix[0]", f"{itt}.inTangent") + cmds.connectAttr(f"{iTan}.Auto", f"{itt}.auto") + cmds.connectAttr(f"{iTan}.Smooth", f"{itt}.smooth") + cmds.connectAttr(f"{iTan}.Weight", f"{itt}.weight") + if not isLast: + cmds.connectAttr(f"{nxt}.worldMatrix[0]", f"{itt}.previousVertex") + else: + cmds.setAttr(f"{iTan}.Smooth", 0.0) + + cmds.connectAttr(f"{end}.worldMatrix[0]", f"{itt}.currentVertex") + cmds.connectAttr(f"{start}.worldMatrix[0]", f"{itt}.nextVertex") + cmds.setAttr(f"{iTan}.Auto", 1.0) + + cmds.connectAttr(f"{itt}.outLinearTarget", f"{ott}.inLinearTarget") + cmds.connectAttr(f"{ott}.outLinearTarget", f"{itt}.inLinearTarget") + + cmds.connectAttr(f"{ott}.out", f"{aoTan}.translate") + cmds.connectAttr(f"{aoTan}.parentInverseMatrix[0]", f"{ott}.parentInverseMatrix") + + cmds.connectAttr(f"{itt}.out", f"{aiTan}.translate") + cmds.connectAttr(f"{aiTan}.parentInverseMatrix[0]", f"{itt}.parentInverseMatrix") + def connectTwistSplineTangents(cvs, twBfrs, oTans, iTans, aoTans, aiTans, closed=False): - """ Connect all of the tangent setups for the spline controller objects - - Arguments: - cvs ([str, ...]): A list of all the CV controllers - twBfrs ([str, ...]): A list of all the Twist Buffers - oTans ([str, ...]): A list of all the out-tangents - iTans ([str, ...]): A list of all the in-tangents - aoTans ([str, ...]): A list of all the auto-out-tangents - aiTans ([str, ...]): A list of all the auto-in-tangents - closed (bool): Whether the spline forms a closed loop - """ - segments = len(cvs) if closed else len(cvs) - 1 - - for i in range(segments): - isFirst = (i == 0) and not closed - isLast = (i == len(cvs) - 2) and not closed - pre = cvs[i - 1] - start = cvs[i] - end = cvs[(i + 1) % len(cvs)] - nxt = cvs[(i + 2) % len(cvs)] - createTangentSegmentSetup(pre, start, end, nxt, oTans[i], iTans[i], aoTans[i], aiTans[i], isFirst=isFirst, isLast=isLast) - - for i in range(segments): - isFirst = (i == 0) and not closed - isLast = (i == len(cvs) - 2) and not closed - - cur = cvs[i] - buf = twBfrs[i] - - pre = cvs[i - 1] if not isFirst else cvs[(i + 2) % len(cvs)] - post = cvs[(i + 1) % len(cvs)] if not isLast else cvs[i - 2] - - createTwistSetup(pre, cur, post, buf, isFirst=isFirst, isLast=isLast) - -def buildTwistSpline(pfx, cvs, aoTans, aiTans, tws, maxParam, master, closed=False): - """ Given all the controller objects, build a twist spline - - Arguments: - pfx (str): The user name of the spline. Will be formatted into the given naming convention - cvs ([str, ...]): A list of the CV objects - aoTans ([str, ...]): A list of the auto-out-tangents - aiTans ([str, ...]): A list of the auto-in-tangents - tws ([str, ...]): A list of the twist controllers - maxParam (float): The U-Value of the last CV - closed (bool): Whether the spline forms a closed loop - - Returns: - str: The spline transform node - str: The spline shape node - - """ - numCVs = len(cvs) # Total number of CV nodes - shift = 0 if closed else 1 # convenience variable so I don't have if's everywhere - usedCVs = numCVs + 1 - shift # Total number of CV's connected to the spline node - - # build the spline object and set the spline Params - splineTfm = cmds.createNode("transform", name=SPLINE_FMT.format(pfx)) - spline = cmds.createNode("twistSpline", parent=splineTfm, name=SPLINE_FMT.format(pfx) + "Shape") - - # Don't connect a first in tangent - for i, aiTan in enumerate(aiTans): - cmds.connectAttr("{}.worldMatrix[0]".format(aiTan), "{}.vertexData[{}].inTangent".format(spline, i+1)) - - # Don't connect a last out tangent - for i, aoTan in enumerate(aoTans): - cmds.connectAttr("{}.worldMatrix[0]".format(aoTan), "{}.vertexData[{}].outTangent".format(spline, i)) - - for u in range(usedCVs): - i = u % numCVs - cmds.connectAttr("{}.worldMatrix[0]".format(cvs[i]), "{}.vertexData[{}].controlVertex".format(spline, u)) - cmds.connectAttr("{}.Pin".format(cvs[i]), "{}.vertexData[{}].paramWeight".format(spline, u)) - if u != i: - # The paramValue needs an offset if we're at the last connection of a closed spline - adL = cmds.createNode("addDoubleLinear") - cmds.setAttr("{0}.input2".format(adL), maxParam) - cmds.connectAttr("{0}.PinParam".format(cvs[i]), "{0}.input1".format(adL), f=True) - cmds.connectAttr("{0}.output".format(adL), "{}.vertexData[{}].paramValue".format(spline, u), f=True) - else: - cmds.connectAttr("{}.PinParam".format(cvs[i]), "{}.vertexData[{}].paramValue".format(spline, u)) - cmds.setAttr("{}.PinParam".format(cvs[i]), (u * maxParam) / (usedCVs - 1.0)) - - cmds.connectAttr("{}.UseTwist".format(tws[i]), "{}.vertexData[{}].twistWeight".format(spline, u)) - cmds.connectAttr("{}.rotateX".format(tws[i]), "{}.vertexData[{}].twistValue".format(spline, u)) - - cmds.setAttr("{}.Pin".format(cvs[0]), 1.0) - cmds.setAttr("{}.UseTwist".format(tws[0]), 1.0) - - # Connect the scaleCompensation parameter - cmds.connectAttr("{}.scaleX".format(master), "{}.scaleCompensation".format(spline)) - - return splineTfm, spline + """Connect all of the tangent setups for the spline controller objects + + Arguments: + cvs ([str, ...]): A list of all the CV controllers + twBfrs ([str, ...]): A list of all the Twist Buffers + oTans ([str, ...]): A list of all the out-tangents + iTans ([str, ...]): A list of all the in-tangents + aoTans ([str, ...]): A list of all the auto-out-tangents + aiTans ([str, ...]): A list of all the auto-in-tangents + closed (bool): Whether the spline forms a closed loop + """ + segments = len(cvs) if closed else len(cvs) - 1 + + for i in range(segments): + isFirst = (i == 0) and not closed + isLast = (i == len(cvs) - 2) and not closed + pre = cvs[i - 1] + start = cvs[i] + end = cvs[(i + 1) % len(cvs)] + nxt = cvs[(i + 2) % len(cvs)] + _createTangentSegmentSetup( + pre, + start, + end, + nxt, + oTans[i], + iTans[i], + aoTans[i], + aiTans[i], + isFirst=isFirst, + isLast=isLast, + ) + + for i in range(segments): + isFirst = (i == 0) and not closed + isLast = (i == len(cvs) - 2) and not closed + + cur = cvs[i] + buf = twBfrs[i] + + pre = cvs[i - 1] if not isFirst else cvs[(i + 2) % len(cvs)] + post = cvs[(i + 1) % len(cvs)] if not isLast else cvs[i - 2] + + twt = cmds.createNode("twistTangent", name="twistAuto") + dcm = cmds.createNode("decomposeMatrix", name="twistDecompose") + + if isLast: + cmds.setAttr(f"{twt}.backpoint", True) + if isFirst or isLast: + cmds.setAttr(f"{twt}.endpoint", True) + + cmds.connectAttr(f"{pre}.worldMatrix[0]", f"{twt}.previousVertex") + cmds.connectAttr(f"{cur}.worldMatrix[0]", f"{twt}.currentVertex") + cmds.connectAttr(f"{post}.worldMatrix[0]", f"{twt}.nextVertex") + + cmds.connectAttr(f"{buf}.parentInverseMatrix[0]", f"{twt}.parentInverseMatrix") + cmds.connectAttr(f"{twt}.outTwistMat", f"{dcm}.inputMatrix") + cmds.connectAttr(f"{dcm}.outputRotate", f"{buf}.rotate") + cmds.connectAttr(f"{dcm}.outputScale", f"{buf}.scale") + cmds.connectAttr(f"{dcm}.outputTranslate", f"{buf}.translate") + + +def connectTwistSplineMultiTangents( + cvs, twBfrs, oCtrls, iCtrls, oBfrs, iBfrs, oRests, iRests, closed=False +): + """Connect all of the tangent setups for the spline controller objects + + Arguments: + cvs ([str, ...]): A list of all the CV controllers + twBfrs ([str, ...]): A list of all the Twist Buffers + oBfrs ([str, ...]): A list of all the out-tangents + iBfrs ([str, ...]): A list of all the in-tangents + oRests ([str, ...]): A list of all the out-tangent rest positions + iRests ([str, ...]): A list of all the in-tangent rest positions + closed (bool): Whether the spline forms a closed loop + """ + + tmt = cmds.createNode("twistMultiTangent", name="twistMultiAuto") + if closed: + cmds.setAttr(f"{tmt}.closed", True) + + for i in range(len(cvs)): + vt = f"{tmt}.vertTans[{i}]" + vd = f"{tmt}.vertData[{i}]" + cv = cvs[i] + twistBuf = twBfrs[i] + + if i != 0: + inTanCtrl = iCtrls[i - 1] + inTanPar = iBfrs[i - 1] + inTanRest = iRests[i - 1] + cmds.connectAttr(f"{inTanPar}.parentInverseMatrix", f"{vd}.inParentInverseMatrix") + cmds.connectAttr(f"{inTanRest}.worldMatrix", f"{vd}.inTanMat") + cmds.connectAttr(f"{vt}.inVertTan", f"{inTanPar}.translate") + cmds.connectAttr(f"{inTanCtrl}.Auto", f"{vd}.inAuto") + cmds.connectAttr(f"{inTanCtrl}.Smooth", f"{vd}.inSmooth") + cmds.connectAttr(f"{inTanCtrl}.Weight", f"{vd}.inTanWeight") + + cmds.connectAttr(f"{cv}.worldInverseMatrix", f"{vd}.twistParentInverseMatrix") + cmds.connectAttr(f"{cv}.worldMatrix", f"{vd}.vertMat") + cmds.connectAttr(f"{vt}.twistMat", f"{twistBuf}.offsetParentMatrix") + + if i != len(cvs) - 1: + outTanCtrl = oCtrls[i] + outTanPar = oBfrs[i] + outTanRest = oRests[i] + cmds.connectAttr(f"{outTanPar}.parentInverseMatrix", f"{vd}.outParentInverseMatrix") + cmds.connectAttr(f"{outTanRest}.worldMatrix", f"{vd}.outTanMat") + cmds.connectAttr(f"{vt}.outVertTan", f"{outTanPar}.translate") + cmds.connectAttr(f"{outTanCtrl}.Auto", f"{vd}.outAuto") + cmds.connectAttr(f"{outTanCtrl}.Smooth", f"{vd}.outSmooth") + cmds.connectAttr(f"{outTanCtrl}.Weight", f"{vd}.outTanWeight") + + +def buildTwistSpline(pfx, cvs, aoTans, aiTans, tws, maxParam, master, closed=False, twistMul=-1.0): + """Given all the controller objects, build a twist spline + + Arguments: + pfx (str): The user name of the spline. Will be formatted into the given naming convention + cvs ([str, ...]): A list of the CV objects + aoTans ([str, ...]): A list of the auto-out-tangents + aiTans ([str, ...]): A list of the auto-in-tangents + tws ([str, ...]): A list of the twist controllers + maxParam (float): The U-Value of the last CV + closed (bool): Whether the spline forms a closed loop + + Returns: + str: The spline transform node + str: The spline shape node + + """ + numCVs = len(cvs) # Total number of CV nodes + shift = 0 if closed else 1 # convenience variable so I don't have if's everywhere + usedCVs = numCVs + 1 - shift # Total number of CV's connected to the spline node + + # build the spline object and set the spline Params + splineTfm = cmds.createNode("transform", name=SPLINE_FMT.format(pfx)) + spline = cmds.createNode("twistSpline", parent=splineTfm, name=f"{SPLINE_FMT.format(pfx)}Shape") + + cmds.setAttr(f"{spline}.twistMultiplier", twistMul) + + # Don't connect a first in tangent + for i, aiTan in enumerate(aiTans): + cmds.connectAttr( + f"{aiTan}.worldMatrix[0]", + f"{spline}.vertexData[{i + 1}].inTangent", + ) + + # Don't connect a last out tangent + for i, aoTan in enumerate(aoTans): + cmds.connectAttr( + f"{aoTan}.worldMatrix[0]", + f"{spline}.vertexData[{i}].outTangent", + ) + + for u in range(usedCVs): + i = u % numCVs + vdu = f"{spline}.vertexData[{u}]" + + cmds.connectAttr( + f"{cvs[i]}.worldMatrix[0]", + f"{vdu}.controlVertex", + ) + cmds.connectAttr(f"{cvs[i]}.Pin", f"{vdu}.paramWeight") + if u != i: + # The paramValue needs an offset if we're at the last connection of a closed spline + adL = cmds.createNode("addDoubleLinear") + cmds.setAttr(f"{adL}.input2", maxParam) + cmds.connectAttr(f"{cvs[i]}.PinParam", f"{adL}.input1", force=True) + cmds.connectAttr(f"{adL}.output", f"{vdu}.paramValue", force=True) + else: + cmds.connectAttr(f"{cvs[i]}.PinParam", f"{vdu}.paramValue") + cmds.setAttr(f"{cvs[i]}.PinParam", (u * maxParam) / (usedCVs - 1.0)) + + cmds.connectAttr(f"{tws[i]}.UseTwist", f"{vdu}.twistWeight") + cmds.connectAttr(f"{tws[i]}.rotateX", f"{vdu}.twistValue") + + cmds.setAttr(f"{cvs[0]}.Pin", 1.0) + cmds.setAttr(f"{tws[0]}.UseTwist", 1.0) + + # Connect the scaleCompensation parameter + cmds.connectAttr(f"{master}.scaleX", f"{spline}.scaleCompensation") + + return splineTfm, spline + def buildRiders(pfx, spline, master, numJoints, closed=False): - """ Build rider joints and constrain them to the spline - - Arguments: - pfx (str): The user name of the spline. Will be formatted into the given naming convention - spline (str): The spline shape node - master (str): The master controller transform ndoe - numJoints (int): The number of joints to create - closed (bool): Whether or not the spline is a closed loop - - Returns: - [str, ...]: The joint parent transforms - [str, ...]: The joints - str: The organizer parent - str: The constraint node - """ - # make the joints at origin. The constraint will put them in place - jointsGrp = cmds.createNode("transform", name=DFM_ORG_FMT.format(pfx)) - jPars, joints = [], [] - for i in range(numJoints): - jp = cmds.createNode("transform", name=DFM_BFR_FMT.format(pfx, i + 1), parent=jointsGrp) - j = cmds.createNode("joint", name=DFM_FMT.format(pfx, i + 1), parent=jp) - cmds.setAttr(j + ".radius", 1.2) - cmds.setAttr(jp + ".displayLocalAxis", 1) - jPars.append(jp) - joints.append(j) - - # build the constraint object - cnst = cmds.createNode("riderConstraint") - cmds.connectAttr("{}.Offset".format(master), "{}.globalOffset".format(cnst)) - cmds.connectAttr("{}.Stretch".format(master), "{}.globalSpread".format(cnst)) - # Connect the scaleCompensation from the spline's twin attribute - cmds.connectAttr("{}.scaleCompensation".format(spline), "{}.scaleCompensation".format(cnst)) - if closed: - cmds.setAttr("{}.useCycle".format(cnst), 1) - - # connect the constraints - cmds.connectAttr("{}.outputSpline".format(spline), "{}.inputSplines[0].spline".format(cnst)) - for i in range(len(jPars)): - if len(jPars) == 1: - cmds.setAttr("{0}.params[{1}].param".format(cnst, i), 0.5) - else: - cmds.setAttr("{0}.params[{1}].param".format(cnst, i), i / (numJoints - 1.0)) - cmds.connectAttr("{}.parentInverseMatrix[0]".format(jPars[i]), "{0}.params[{1}].parentInverseMatrix".format(cnst, i)) - cmds.connectAttr("{0}.outputs[{1}].translate".format(cnst, i), "{}.translate".format(jPars[i])) - cmds.connectAttr("{0}.outputs[{1}].rotate".format(cnst, i), "{}.rotate".format(jPars[i])) - cmds.connectAttr("{0}.outputs[{1}].scale".format(cnst, i), "{}.scale".format(jPars[i])) - - return jPars, joints, jointsGrp, cnst - -def makeTwistSpline(pfx, numCVs, numJoints=10, maxParam=None, spread=1.0, closed=False): - """ Make a twist spline - - Arguments: - pfx (str): The user name of the spline. Will be formatted into the given naming convention - numCVs (int): The number of CV's to make that control the spline - numJoints (int): The number of joints to make that ride the spline. Defaults to 10 - maxParam (int): The U-Value of the last CV. Defaults to 3*spread*(numCVs - 1) - spread (float): The distance between each controller (including tangents). Defaults to 1 - closed (bool): Whether the spline forms a closed loop - - Returns: - [str, ...]: All the CV's - [str, ...]: All the CV's parent transforms - [str, ...]: All the Out-Tangents - [str, ...]: All the In-Tangents - [str, ...]: All the joint parents - [str, ...]: All the joints - str: The joint organizer object (None if no joints requested) - str: The spline object transform - str: The base controller - str: The rider constraint (None if no joints requested) - """ - if numCVs < 2: - raise ValueError("Cannot create a TwistSpline with less than 2 CVs") - - if not cmds.pluginInfo("TwistSpline", query=True, loaded=True): - cmds.loadPlugin("TwistSpline") - - if maxParam is None: - maxParam = (numCVs - 1) - - maxParam *= 3.0 * spread - - cvs, cvBfrs, oTans, iTans, aoTans, aiTans, tws, twBfrs, master = mkTwistSplineControllers(pfx, numCVs, spread, closed=closed) - connectTwistSplineTangents(cvs, twBfrs, oTans, iTans, aoTans, aiTans, closed=closed) - splineTfm, splineShape = buildTwistSpline(pfx, cvs, aoTans, aiTans, tws, maxParam, master, closed=closed) - - jPars, joints, group, cnst = None, None, None, None - if numJoints > 0: - jPars, joints, group, cnst = buildRiders(pfx, splineShape, master, numJoints, closed=closed) - - # The scale of the overall spline should not affect the scale of the riders for now - # Eventually, the rider constraint will handle interpolated scales - #dnMultDivide = cmds.createNode("multiplyDivide") - #cmds.setAttr("{0}.operation".format(dnMultDivide), 2) - #cmds.setAttr("{0}.input1".format(dnMultDivide), 1, 1, 1) - #cmds.connectAttr("{0}.scale".format(master), "{0}.input2".format(dnMultDivide), f=True) - #cmds.connectAttr("{0}.outputX".format(dnMultDivide), "{0}.normValue".format(cnst), f=True) - - return cvs, cvBfrs, oTans, iTans, jPars, joints, group, splineTfm, master, cnst + """Build rider joints and constrain them to the spline + + Arguments: + pfx (str): The user name of the spline. Will be formatted into the given naming convention + spline (str): The spline shape node + master (str): The master controller transform ndoe + numJoints (int): The number of joints to create + closed (bool): Whether or not the spline is a closed loop + + Returns: + [str, ...]: The joint parent transforms + [str, ...]: The joints + str: The organizer parent + str: The constraint node + """ + # make the joints at origin. The constraint will put them in place + jointsGrp = cmds.createNode("transform", name=DFM_ORG_FMT.format(pfx)) + jPars, joints = [], [] + for i in range(numJoints): + jp = cmds.createNode("transform", name=DFM_BFR_FMT.format(pfx, i + 1), parent=jointsGrp) + j = cmds.createNode("joint", name=DFM_FMT.format(pfx, i + 1), parent=jp) + cmds.setAttr(f"{j}.radius", 1.2) + cmds.setAttr(f"{jp}.displayLocalAxis", 1) + jPars.append(jp) + joints.append(j) + + # build the constraint object + cnst = cmds.createNode("riderConstraint") + cmds.connectAttr(f"{master}.Offset", f"{cnst}.globalOffset") + cmds.connectAttr(f"{master}.Stretch", f"{cnst}.globalSpread") + # Connect the scaleCompensation from the spline's twin attribute + cmds.connectAttr(f"{spline}.scaleCompensation", f"{cnst}.scaleCompensation") + if closed: + cmds.setAttr(f"{cnst}.useCycle", 1) + + # connect the constraints + cmds.connectAttr(f"{spline}.outputSpline", f"{cnst}.inputSplines[0].spline") + for i in range(len(jPars)): + if len(jPars) == 1: + cmds.setAttr(f"{cnst}.params[{i}].param", 0.5) + else: + cmds.setAttr(f"{cnst}.params[{i}].param", i / (numJoints - 1.0)) + cmds.connectAttr( + f"{jPars[i]}.parentInverseMatrix[0]", + f"{cnst}.params[{i}].parentInverseMatrix", + ) + cmds.connectAttr( + f"{cnst}.outputs[{i}].translate", + f"{jPars[i]}.translate", + ) + cmds.connectAttr(f"{cnst}.outputs[{i}].rotate", f"{jPars[i]}.rotate") + cmds.connectAttr(f"{cnst}.outputs[{i}].scale", f"{jPars[i]}.scale") + + return jPars, joints, jointsGrp, cnst + + +def makeTwistSpline( + pfx, numCVs, numJoints=10, maxParam=None, spread=1.0, closed=False, singleTangentNode=True +): + """Make a twist spline + + Arguments: + pfx (str): The user name of the spline. Will be formatted into the given naming convention + numCVs (int): The number of CV's to make that control the spline + numJoints (int): The number of joints to make that ride the spline. Defaults to 10 + maxParam (int): The U-Value of the last CV. Defaults to 3*spread*(numCVs - 1) + spread (float): The distance between each controller (including tangents). Defaults to 1 + closed (bool): Whether the spline forms a closed loop + + Returns: + [str, ...]: All the CV's + [str, ...]: All the CV's parent transforms + [str, ...]: All the Out-Tangents + [str, ...]: All the In-Tangents + [str, ...]: All the joint parents + [str, ...]: All the joints + str: The joint organizer object (None if no joints requested) + str: The spline object transform + str: The base controller + str: The rider constraint (None if no joints requested) + """ + if numCVs < 2: + raise ValueError("Cannot create a TwistSpline with less than 2 CVs") + + if not cmds.pluginInfo("TwistSpline", query=True, loaded=True): + cmds.loadPlugin("TwistSpline") + + if maxParam is None: + maxParam = numCVs - 1 + + maxParam *= 3.0 * spread + + cvs, cvBfrs, oBfrs, iBfrs, oRests, iRests, oCtrls, iCtrls, tws, twBfrs, master = ( + mkTwistSplineControllers( + pfx, numCVs, spread, closed=closed, singleTangentNode=singleTangentNode + ) + ) + + if singleTangentNode: + connectTwistSplineMultiTangents( + cvs, twBfrs, oCtrls, iCtrls, oBfrs, iBfrs, oRests, iRests, closed=closed + ) + else: + connectTwistSplineTangents(cvs, twBfrs, oCtrls, iCtrls, oBfrs, iBfrs, closed=closed) + + splineTfm, splineShape = buildTwistSpline( + pfx, cvs, oCtrls, iCtrls, tws, maxParam, master, closed=closed, twistMul=-1.0 + ) + + jPars, joints, group, cnst = None, None, None, None + if numJoints > 0: + jPars, joints, group, cnst = buildRiders(pfx, splineShape, master, numJoints, closed=closed) + + # The scale of the overall spline should not affect the scale of the riders for now + # Eventually, the rider constraint will handle interpolated scales + # dnMultDivide = cmds.createNode("multiplyDivide") + # cmds.setAttr(f"{dnMultDivide}.operation", 2) + # cmds.setAttr(f"{dnMultDivide}.input1", 1, 1, 1) + # cmds.connectAttr(f"{master}.scale", f"{dnMultDivide}.input2", force=True) + # cmds.connectAttr(f"{dnMultDivide}.outputX", f"{cnst}.normValue", force=True) + + return cvs, cvBfrs, oCtrls, iCtrls, jPars, joints, group, splineTfm, master, cnst -def _bezierConvert(crv): - """ Convert a curve to bezier non-destructively - Arguments: - crv (str): A curve shape or transform - - Returns: - str: A Bezier curve shape - list: Any temporary objects that need to be deleted later - """ - # Get the bezier shapes - bezShapes = cmds.listRelatives(crv, path=True, type="bezierCurve") - if bezShapes is not None: - return bezShapes[0], [] - - nurbsShapes = cmds.listRelatives(crv, path=True, type="nurbsCurve") - if nurbsShapes is not None: - tfm = cmds.listRelatives(nurbsShapes, path=True, parent=True) - # nurbsCurveToBezier is *supposed* to take arguments, but always errors - # So I have to do this with selection. I hate that. A Lot. - dup = cmds.duplicate(tfm) # duplicates and selects the object - cmds.select(dup) - cmds.nurbsCurveToBezier() # Converts selection to bezier - bezShapes = cmds.listRelatives(dup[0], path=True, type="bezierCurve") - if bezShapes is not None: - return bezShapes[0], dup - - return None, [] - -def convertToTwistSpline(pfx, crv, numJoints=10): - """ Convert a given NURBS or Bezier curve to a TwistSpline - - Arguments: - pfx (str): The user name of the spline. Will be formatted into the given naming convention - crv (str): The transform or shape of a *bezier* spline - numJoints (int): The number of joints to create that ride this spline - """ - # get nurbs curve shape - crvShape, toDelete = _bezierConvert(crv) - - # Get the curve function set - # There's no way to get the knots through pure MEL (nodes don't count) - # So as long as I'm doing it this way, I'll do it all like this - objects = OpenMaya.MSelectionList() - OpenMaya.MGlobal.getSelectionListByName(crvShape, objects) - meshDag = OpenMaya.MDagPath() - objects.getDagPath(0, meshDag) - curveFn = OpenMaya.MFnNurbsCurve(meshDag) - - # Get the curve data - knots = OpenMaya.MDoubleArray() - curveFn.getKnots(knots) - params = list(knots)[1::3] - numCVs = len(params) - curveLen = curveFn.length() - - # Maya reports the wrong form of the curve through the API - # So I've got to do it via mel - #curveForm = curveFn.form() - curveForm = cmds.getAttr("{0}.form".format(crvShape)) - isClosed = curveForm > 0 # 1->closed 2->periodic - if isClosed: - numCVs -= 1 - - # Get the point data - # I could do it with cmds if I wanted, but I would have to un-flatten the list - # it's annoying either way - #allPos = cmds.xform("{0}.cv[*]".format(crvShape), q=True, worldSpace=True, translation=True) - allPos = OpenMaya.MPointArray() - curveFn.getCVs(allPos) - # Just testing a micro-optimization - # 2 steps means not creating 3 MPoints per loop - allPos = [allPos[i] for i in range(allPos.length())] - allPos = [(p.x, p.y, p.z) for p in allPos] - - # Build the spline - tempRet = makeTwistSpline(pfx, numCVs, numJoints=numJoints, maxParam=curveLen / 3.0, spread=1.0, closed=isClosed) - cvs, bfrs, oTans, iTans, jPars, joints, group, spline, master, riderCnst = tempRet - - # Set the positions - for pos, cv in zip(allPos[::3], bfrs): - cmds.xform(cv, ws=True, a=True, t=pos) - - # Pin all the controllers so no length preservation happens - # That way we can get the rotations at each param - for cv in cvs: - cmds.setAttr("{0}.Pin".format(cv), 1) - - for pos, cv in zip(allPos[1::3], oTans): - cmds.xform(cv, ws=True, a=True, t=pos) - cmds.setAttr("{0}.Auto".format(cv), 0) - - for pos, cv in zip(allPos[2::3], iTans): - cmds.xform(cv, ws=True, a=True, t=pos) - cmds.setAttr("{0}.Auto".format(cv), 0) - - # Make sure there is a rider constraint so I can follow the twist all along the spline - tmpCnst = cmds.createNode("riderConstraint") - cmds.connectAttr("{}.outputSpline".format(spline), "{}.inputSplines[0].spline".format(tmpCnst)) - - # Get the rotations at each CV point - newInd = 0 - rotations = [] - cmds.setAttr("{0}.normValue".format(tmpCnst), params[-1]) - for param in params: - cmds.setAttr("{0}.params[{1}].param".format(tmpCnst, newInd), param) - cmds.dgeval(tmpCnst) # maybe a propagation bug somewhere in the constraint? - rot = cmds.getAttr("{0}.outputs[{1}].rotate".format(tmpCnst, newInd)) - rotations.append(rot[0]) - cmds.delete(tmpCnst) - - # Update the rotations after I've got them all - for rot, ctrl in zip(rotations, bfrs): - cmds.setAttr("{0}.rotate".format(ctrl), *rot) - - # Un-pin everything but the first, so back to length preservation - for cv in cvs[1:]: - cmds.setAttr("{0}.Pin".format(cv), 0) - - # Re-set the tangent worldspace positions now that things have changed - for pos, cv in zip(allPos[1::3], oTans): - cmds.xform(cv, ws=True, a=True, t=pos) - cmds.setAttr("{0}.Auto".format(cv), 0) - - for pos, cv in zip(allPos[2::3], iTans): - cmds.xform(cv, ws=True, a=True, t=pos) - cmds.setAttr("{0}.Auto".format(cv), 0) - - # Delete the extra joint group and the constraint if I had to make 'em - if toDelete: - cmds.delete(toDelete) - - # Lock the buffers - for bfr in bfrs: - for att in [x+y for x in 'trs' for y in 'xyz']: - cmds.setAttr("{0}.{1}".format(bfr, att), lock=True) - - -def convertSelectedToTwistSpline(pfx, numJoints=10): - sel = cmds.ls(sl=True) - for s in sel: - convertToTwistSpline(pfx, s, numJoints=numJoints) +def _bezierConvert(crv): + """Convert a curve to bezier non-destructively + Arguments: + crv (str): A curve shape or transform + + Returns: + str: A Bezier curve shape + list: Any temporary objects that need to be deleted later + """ + # Get the bezier shapes + bezShapes = cmds.listRelatives(crv, path=True, type="bezierCurve") + if bezShapes is not None: + return bezShapes[0], [] + + nurbsShapes = cmds.listRelatives(crv, path=True, type="nurbsCurve") + if nurbsShapes is not None: + tfm = cmds.listRelatives(nurbsShapes, path=True, parent=True) + # nurbsCurveToBezier is *supposed* to take arguments, but always errors + # So I have to do this with selection. I hate that. A Lot. + dup = cmds.duplicate(tfm) # duplicates and selects the object + cmds.select(dup) + cmds.nurbsCurveToBezier() # Converts selection to bezier + bezShapes = cmds.listRelatives(dup[0], path=True, type="bezierCurve") + if bezShapes is not None: + return bezShapes[0], dup + + return None, [] + + +def convertToTwistSpline(pfx, crv, numJoints=10, singleTangentNode=True): + """Convert a given NURBS or Bezier curve to a TwistSpline + + Arguments: + pfx (str): The user name of the spline. Will be formatted into the given naming convention + crv (str): The transform or shape of a *bezier* spline + numJoints (int): The number of joints to create that ride this spline + """ + # get nurbs curve shape + crvShape, toDelete = _bezierConvert(crv) + + # Get the curve function set + # There's no way to get the knots through pure MEL (nodes don't count) + # So as long as I'm doing it this way, I'll do it all like this + objects = OpenMaya.MSelectionList() + OpenMaya.MGlobal.getSelectionListByName(crvShape, objects) + meshDag = OpenMaya.MDagPath() + objects.getDagPath(0, meshDag) + curveFn = OpenMaya.MFnNurbsCurve(meshDag) + + # Get the curve data + knots = OpenMaya.MDoubleArray() + curveFn.getKnots(knots) + params = list(knots)[1::3] + numCVs = len(params) + curveLen = curveFn.length() + + # Maya reports the wrong form of the curve through the API + # So I've got to do it via mel + # curveForm = curveFn.form() + curveForm = cmds.getAttr(f"{crvShape}.form") + isClosed = curveForm > 0 # 1->closed 2->periodic + if isClosed: + numCVs -= 1 + + # Get the point data + # I could do it with cmds if I wanted, but I would have to un-flatten the list + # it's annoying either way + # allPos = cmds.xform(f"{crvShape}.cv[*]", query=True, worldSpace=True, translation=True) + allPos = OpenMaya.MPointArray() + curveFn.getCVs(allPos) + # Just testing a micro-optimization + # 2 steps means not creating 3 MPoints per loop + allPos = [allPos[i] for i in range(allPos.length())] + allPos = [(p.x, p.y, p.z) for p in allPos] + + # Build the spline + tempRet = makeTwistSpline( + pfx, + numCVs, + numJoints=numJoints, + maxParam=curveLen / 3.0, + spread=1.0, + closed=isClosed, + singleTangentNode=singleTangentNode, + ) + cvs, bfrs, oTans, iTans, jPars, joints, group, spline, master, riderCnst = tempRet + + # Set the positions + for pos, cv in zip(allPos[::3], bfrs): + cmds.xform(cv, worldSpace=True, absolute=True, translation=pos) + + # Pin all the controllers so no length preservation happens + # That way we can get the rotations at each param + for cv in cvs: + cmds.setAttr(f"{cv}.Pin", 1) + + for pos, cv in zip(allPos[1::3], oTans): + cmds.xform(cv, worldSpace=True, absolute=True, translation=pos) + cmds.setAttr(f"{cv}.Auto", 0) + + for pos, cv in zip(allPos[2::3], iTans): + cmds.xform(cv, worldSpace=True, absolute=True, translation=pos) + cmds.setAttr(f"{cv}.Auto", 0) + + # Make sure there is a rider constraint so I can follow the twist all along the spline + tmpCnst = cmds.createNode("riderConstraint") + cmds.connectAttr(f"{spline}.outputSpline", f"{tmpCnst}.inputSplines[0].spline") + + # Get the rotations at each CV point + newInd = 0 + rotations = [] + cmds.setAttr(f"{tmpCnst}.normValue", params[-1]) + for param in params: + cmds.setAttr(f"{tmpCnst}.params[{newInd}].param", param) + cmds.dgeval(tmpCnst) # maybe a propagation bug somewhere in the constraint? + rot = cmds.getAttr(f"{tmpCnst}.outputs[{newInd}].rotate") + rotations.append(rot[0]) + cmds.delete(tmpCnst) + + # Update the rotations after I've got them all + for rot, ctrl in zip(rotations, bfrs): + cmds.setAttr(f"{ctrl}.rotate", *rot) + + # Un-pin everything but the first, so back to length preservation + for cv in cvs[1:]: + cmds.setAttr(f"{cv}.Pin", 0) + + # Re-set the tangent worldspace positions now that things have changed + for pos, cv in zip(allPos[1::3], oTans): + cmds.xform(cv, worldSpace=True, absolute=True, translation=pos) + cmds.setAttr(f"{cv}.Auto", 0) + + for pos, cv in zip(allPos[2::3], iTans): + cmds.xform(cv, worldSpace=True, absolute=True, translation=pos) + cmds.setAttr(f"{cv}.Auto", 0) + + # Delete the extra joint group and the constraint if I had to make 'em + if toDelete: + cmds.delete(toDelete) + + # Lock the buffers + for bfr in bfrs: + for att in [x + y for x in "trs" for y in "xyz"]: + cmds.setAttr(f"{bfr}.{att}", lock=True) + + +def convertSelectedToTwistSpline(pfx, numJoints=10, singleTangentNode=True): + sel = cmds.ls(sl=True) + for s in sel: + convertToTwistSpline(pfx, s, numJoints=numJoints, singleTangentNode=singleTangentNode) diff --git a/src/pluginMain.cpp b/src/pluginMain.cpp index d761812..9e53adf 100644 --- a/src/pluginMain.cpp +++ b/src/pluginMain.cpp @@ -26,6 +26,7 @@ SOFTWARE. #include "twistSplineData.h" #include "riderConstraint.h" #include "twistTangentNode.h" +#include "twistMultiTangentNode.h" #include "drawOverride.h" #include @@ -64,8 +65,6 @@ MStatus initializePlugin( MObject obj ) { return status; } - - status = plugin.registerNode( "twistTangent", TwistTangentNode::id, @@ -77,6 +76,17 @@ MStatus initializePlugin( MObject obj ) { return status; } + status = plugin.registerNode( + "twistMultiTangent", + TwistMultiTangentNode::id, + TwistMultiTangentNode::creator, + TwistMultiTangentNode::initialize + ); + if (!status) { + status.perror("Failed to register TwistMultiTangent Node"); + return status; + } + status = MHWRender::MDrawRegistry::registerGeometryOverrideCreator( TwistSplineNode::drawDbClassification, TwistSplineNode::drawRegistrantId, @@ -90,7 +100,7 @@ MStatus initializePlugin( MObject obj ) { } MStatus uninitializePlugin(MObject obj) { - MStatus nodeStat, dataStat, drawStat, riderStat, tangentStat; + MStatus nodeStat, dataStat, drawStat, riderStat, tangentStat, mtangentStat; MFnPlugin plugin(obj); dataStat = plugin.deregisterData(TwistSplineData::id); @@ -105,6 +115,9 @@ MStatus uninitializePlugin(MObject obj) { tangentStat = plugin.deregisterNode(TwistTangentNode::id); if (!tangentStat) nodeStat.perror("Failed to de-register twistTangent Node"); + mtangentStat = plugin.deregisterNode(TwistMultiTangentNode::id); + if (!mtangentStat) nodeStat.perror("Failed to de-register twistMultiTangent Node"); + drawStat = MDrawRegistry::deregisterGeometryOverrideCreator( TwistSplineNode::drawDbClassification, TwistSplineNode::drawRegistrantId); @@ -114,5 +127,6 @@ MStatus uninitializePlugin(MObject obj) { if (!dataStat) return dataStat; if (!riderStat) return riderStat; if (!tangentStat) return tangentStat; + if (!mtangentStat) return mtangentStat; return drawStat; } diff --git a/src/twistMultiTangentNode.cpp b/src/twistMultiTangentNode.cpp new file mode 100644 index 0000000..ca94d1a --- /dev/null +++ b/src/twistMultiTangentNode.cpp @@ -0,0 +1,677 @@ +/* +MIT License + +Copyright (c) 2018 Blur Studio + +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. +*/ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "twistMultiTangentNode.h" + +#define CHECKSTAT(m) \ + if (!status) { \ + status.perror(m); \ + return status; \ + }; + +MTypeId TwistMultiTangentNode::id(0x00122715); + +// inputs +MObject TwistMultiTangentNode::aVertMat; +MObject TwistMultiTangentNode::aInTanMat; +MObject TwistMultiTangentNode::aOutTanMat; +MObject TwistMultiTangentNode::aInTanWeight; +MObject TwistMultiTangentNode::aOutTanWeight; +MObject TwistMultiTangentNode::aInTanAuto; +MObject TwistMultiTangentNode::aOutTanAuto; +MObject TwistMultiTangentNode::aInSmooth; +MObject TwistMultiTangentNode::aOutSmooth; +MObject TwistMultiTangentNode::aInParentInv; +MObject TwistMultiTangentNode::aOutParentInv; +MObject TwistMultiTangentNode::aTwistParentInv; +MObject TwistMultiTangentNode::aVertData; + +MObject TwistMultiTangentNode::aStartTension; +MObject TwistMultiTangentNode::aEndTension; +MObject TwistMultiTangentNode::aMaxVertices; +MObject TwistMultiTangentNode::aClosed; + +// outputs +MObject TwistMultiTangentNode::aInTanX; +MObject TwistMultiTangentNode::aInTanY; +MObject TwistMultiTangentNode::aInTanZ; +MObject TwistMultiTangentNode::aInTan; +MObject TwistMultiTangentNode::aOutTanX; +MObject TwistMultiTangentNode::aOutTanY; +MObject TwistMultiTangentNode::aOutTanZ; +MObject TwistMultiTangentNode::aOutTan; +MObject TwistMultiTangentNode::aInTanLen; +MObject TwistMultiTangentNode::aOutTanLen; +MObject TwistMultiTangentNode::aOutTwistUpX; +MObject TwistMultiTangentNode::aOutTwistUpY; +MObject TwistMultiTangentNode::aOutTwistUpZ; +MObject TwistMultiTangentNode::aOutTwistUp; +MObject TwistMultiTangentNode::aOutTwistMat; +MObject TwistMultiTangentNode::aTangents; + +TwistMultiTangentNode::TwistMultiTangentNode() {} +TwistMultiTangentNode::~TwistMultiTangentNode() {} + +void *TwistMultiTangentNode::creator() { return new TwistMultiTangentNode(); } + +MStatus TwistMultiTangentNode::initialize() +{ + MFnNumericAttribute num_attr; + MFnCompoundAttribute cmp_attr; + MFnMatrixAttribute mat_attr; + MFnUnitAttribute unit_attr; + MStatus status; + + aInTanMat = mat_attr.create("inTanMat", "itm"); + mat_attr.setKeyable(false); + mat_attr.setStorable(false); + mat_attr.setHidden(true); + + aOutTanMat = mat_attr.create("outTanMat", "otm"); + mat_attr.setKeyable(false); + mat_attr.setStorable(false); + mat_attr.setHidden(true); + + aInTanWeight = + num_attr.create("inTanWeight", "itw", MFnNumericData::kDouble, 1.0, &status); + num_attr.setKeyable(true); + num_attr.setStorable(false); + num_attr.setSoftMin(0.0); + num_attr.setSoftMax(2.0); + + aOutTanWeight = + num_attr.create("outTanWeight", "otw", MFnNumericData::kDouble, 1.0, &status); + num_attr.setKeyable(true); + num_attr.setStorable(false); + num_attr.setSoftMin(0.0); + num_attr.setSoftMax(2.0); + + aInSmooth = num_attr.create("inSmooth", "ism", MFnNumericData::kDouble, 1.0, &status); + num_attr.setKeyable(true); + num_attr.setStorable(false); + num_attr.setSoftMin(0.0); + num_attr.setSoftMax(1.0); + + aOutSmooth = num_attr.create("outSmooth", "osm", MFnNumericData::kDouble, 1.0, &status); + num_attr.setKeyable(true); + num_attr.setStorable(false); + num_attr.setSoftMin(0.0); + num_attr.setSoftMax(1.0); + + aInTanAuto = num_attr.create("inAuto", "iat", MFnNumericData::kDouble, 1.0, &status); + num_attr.setKeyable(true); + num_attr.setStorable(false); + num_attr.setSoftMin(0.0); + num_attr.setSoftMax(1.0); + + aOutTanAuto = num_attr.create("outAuto", "oat", MFnNumericData::kDouble, 1.0, &status); + num_attr.setKeyable(true); + num_attr.setStorable(false); + num_attr.setSoftMin(0.0); + num_attr.setSoftMax(1.0); + + aVertMat = mat_attr.create("vertMat", "vm"); + mat_attr.setKeyable(false); + mat_attr.setStorable(false); + mat_attr.setHidden(true); + + aInParentInv = mat_attr.create("inParentInverseMatrix", "ipim"); + mat_attr.setKeyable(false); + mat_attr.setStorable(false); + mat_attr.setHidden(true); + + aOutParentInv = mat_attr.create("outParentInverseMatrix", "opim"); + mat_attr.setKeyable(false); + mat_attr.setStorable(false); + mat_attr.setHidden(true); + + aTwistParentInv = mat_attr.create("twistParentInverseMatrix", "tpim"); + mat_attr.setKeyable(false); + mat_attr.setStorable(false); + mat_attr.setHidden(true); + + aVertData = cmp_attr.create("vertData", "vd", &status); + cmp_attr.setArray(true); + cmp_attr.setDisconnectBehavior(cmp_attr.kDelete); + cmp_attr.addChild(aVertMat); + cmp_attr.addChild(aInParentInv); + cmp_attr.addChild(aOutParentInv); + cmp_attr.addChild(aTwistParentInv); + cmp_attr.addChild(aInTanWeight); + cmp_attr.addChild(aOutTanWeight); + cmp_attr.addChild(aInSmooth); + cmp_attr.addChild(aOutSmooth); + cmp_attr.addChild(aInTanAuto); + cmp_attr.addChild(aOutTanAuto); + cmp_attr.addChild(aInTanMat); + cmp_attr.addChild(aOutTanMat); + + addAttribute(aVertData); + + aStartTension = + num_attr.create("startTension", "st", MFnNumericData::kDouble, 2.0, &status); + num_attr.setKeyable(true); + num_attr.setStorable(true); + addAttribute(aStartTension); + + aEndTension = + num_attr.create("endTension", "et", MFnNumericData::kDouble, 2.0, &status); + num_attr.setKeyable(true); + num_attr.setStorable(true); + addAttribute(aEndTension); + + aMaxVertices = num_attr.create("maxVertices", "mv", MFnNumericData::kInt, 999, &status); + num_attr.setKeyable(true); + num_attr.setStorable(true); + num_attr.setMin(2); + addAttribute(aMaxVertices); + + aClosed = num_attr.create("closed", "cl", MFnNumericData::kBoolean, false, &status); + num_attr.setKeyable(true); + num_attr.setStorable(true); + num_attr.setMin(2); + addAttribute(aClosed); + + aInTanX = + unit_attr.create("inVertTanX", "ivx", MFnUnitAttribute::kDistance, 0.0, &status); + unit_attr.setKeyable(false); + unit_attr.setStorable(false); + unit_attr.setWritable(false); + aInTanY = + unit_attr.create("inVertTanY", "ivy", MFnUnitAttribute::kDistance, 0.0, &status); + unit_attr.setKeyable(false); + unit_attr.setStorable(false); + unit_attr.setWritable(false); + aInTanZ = + unit_attr.create("inVertTanZ", "ivz", MFnUnitAttribute::kDistance, 0.0, &status); + unit_attr.setKeyable(false); + unit_attr.setStorable(false); + unit_attr.setWritable(false); + + aInTan = num_attr.create("inVertTan", "ivt", aInTanX, aInTanY, aInTanZ, &status); + num_attr.setKeyable(false); + num_attr.setStorable(false); + num_attr.setWritable(false); + + aOutTanX = + unit_attr.create("outVertTanX", "ovx", MFnUnitAttribute::kDistance, 0.0, &status); + unit_attr.setKeyable(false); + unit_attr.setStorable(false); + unit_attr.setWritable(false); + aOutTanY = + unit_attr.create("outVertTanY", "ovy", MFnUnitAttribute::kDistance, 0.0, &status); + unit_attr.setKeyable(false); + unit_attr.setStorable(false); + unit_attr.setWritable(false); + aOutTanZ = + unit_attr.create("outVertTanZ", "ovz", MFnUnitAttribute::kDistance, 0.0, &status); + unit_attr.setKeyable(false); + unit_attr.setStorable(false); + unit_attr.setWritable(false); + + aOutTan = num_attr.create("outVertTan", "ovt", aOutTanX, aOutTanY, aOutTanZ, &status); + num_attr.setKeyable(false); + num_attr.setStorable(false); + num_attr.setWritable(false); + + aOutTwistUpX = + unit_attr.create("twistUpX", "tux", MFnUnitAttribute::kDistance, 0.0, &status); + unit_attr.setKeyable(false); + unit_attr.setStorable(false); + unit_attr.setWritable(false); + aOutTwistUpY = + unit_attr.create("twistUpY", "tuy", MFnUnitAttribute::kDistance, 0.0, &status); + unit_attr.setKeyable(false); + unit_attr.setStorable(false); + unit_attr.setWritable(false); + aOutTwistUpZ = + unit_attr.create("twistUpZ", "tuz", MFnUnitAttribute::kDistance, 0.0, &status); + unit_attr.setKeyable(false); + unit_attr.setStorable(false); + unit_attr.setWritable(false); + + aOutTwistUp = num_attr.create( + "twistUp", "tu", aOutTwistUpX, aOutTwistUpY, aOutTwistUpZ, &status + ); + num_attr.setKeyable(false); + num_attr.setStorable(false); + num_attr.setWritable(false); + + aOutTwistMat = mat_attr.create("twistMat", "tm"); + mat_attr.setKeyable(false); + mat_attr.setWritable(false); + mat_attr.setStorable(false); + mat_attr.setHidden(true); + + aInTanLen = num_attr.create("inTanLen", "itl", MFnNumericData::kDouble, 0.0, &status); + num_attr.setKeyable(false); + num_attr.setWritable(false); + num_attr.setStorable(false); + aOutTanLen = num_attr.create("outTanLen", "otl", MFnNumericData::kDouble, 0.0, &status); + num_attr.setKeyable(false); + num_attr.setWritable(false); + num_attr.setStorable(false); + + aTangents = cmp_attr.create("vertTans", "vt", &status); + cmp_attr.addChild(aInTan); + cmp_attr.addChild(aOutTan); + cmp_attr.addChild(aInTanLen); + cmp_attr.addChild(aOutTanLen); + cmp_attr.addChild(aOutTwistUp); + cmp_attr.addChild(aOutTwistMat); + + cmp_attr.setArray(true); + cmp_attr.setUsesArrayDataBuilder(true); + cmp_attr.setKeyable(false); + cmp_attr.setWritable(false); + cmp_attr.setStorable(false); + addAttribute(aTangents); + + std::vector ins, outs; + ins.push_back(&aStartTension); + ins.push_back(&aEndTension); + ins.push_back(&aMaxVertices); + ins.push_back(&aClosed); + ins.push_back(&aVertMat); + ins.push_back(&aInParentInv); + ins.push_back(&aOutParentInv); + ins.push_back(&aTwistParentInv); + ins.push_back(&aInTanWeight); + ins.push_back(&aOutTanWeight); + ins.push_back(&aInTanMat); + ins.push_back(&aOutTanMat); + ins.push_back(&aInSmooth); + ins.push_back(&aOutSmooth); + ins.push_back(&aInTanAuto); + ins.push_back(&aOutTanAuto); + ins.push_back(&aVertData); + + outs.push_back(&aInTanX); + outs.push_back(&aInTanY); + outs.push_back(&aInTanZ); + outs.push_back(&aInTan); + outs.push_back(&aOutTanX); + outs.push_back(&aOutTanY); + outs.push_back(&aOutTanZ); + outs.push_back(&aOutTan); + outs.push_back(&aInTanLen); + outs.push_back(&aOutTanLen); + outs.push_back(&aOutTwistUpX); + outs.push_back(&aOutTwistUpY); + outs.push_back(&aOutTwistUpZ); + outs.push_back(&aOutTwistUp); + outs.push_back(&aOutTwistMat); + outs.push_back(&aTangents); + + for (auto i : ins) { + for (auto o : outs) { + attributeAffects(*i, *o); + } + } + + return MS::kSuccess; +} + +struct DirTanData { + double weight; + double smooth; + double autoVal; + MMatrix piMat; + MMatrix userMat; + MVector leg; + double legLen; + MVector normLeg; + MVector smoothTan; + MVector linearTan; + MVector doneTan; +}; + +struct TanData { + MMatrix tfmMat; + MMatrix smoothMat; + MVector tfm; + MVector norm; + MMatrix piTwist; + DirTanData inTan; + DirTanData outTan; +}; + +MMatrix buildMat(const MVector &tfm, const MVector &inrm, const MVector &tan) +{ + //"""Build a matrix from a position, tangent and normal Go through Graham-Schmidt to + //orthonormalize it """ + MVector bin = (tan ^ inrm).normal(); + MVector nrm = (bin ^ tan).normal(); + + MMatrix ret; + ret[0][0] = tan[0]; + ret[0][1] = tan[1]; + ret[0][2] = tan[2]; + + ret[1][0] = nrm[0]; + ret[1][1] = nrm[1]; + ret[1][2] = nrm[2]; + + ret[2][0] = bin[0]; + ret[2][1] = bin[1]; + ret[2][2] = bin[2]; + + ret[3][0] = tfm[0]; + ret[3][1] = tfm[1]; + ret[3][2] = tfm[2]; + return ret; +} + +MVector +buildVertMatTangent(double nextLegLen, double preLegLen, MVector &nextLegNrm, MVector &preLegNrm) +{ + //"""Given all this pre-calculated data, figure out the normalized tangent to the curve at the + //given vertex""" + if (preLegLen == 0.0) { + if (nextLegLen == 0.0) { + return MVector(0, 1, 0); + } + return nextLegNrm; + } + + if (nextLegLen == 0.0) { + return -preLegNrm; + } + + double dot = preLegNrm * nextLegNrm; + + // Pre and post legs both point TOWARD the curTFM + if (dot > 0.999999999) { + // pre/next legs are pointing the same direction + // so we've got a 180 turnaround + MVector y; + if (std::abs(preLegNrm * y) > 0.999999999) { + y = MVector(1, 0, 0); + } + else { + y = MVector(0, 1, 0); + } + return (preLegNrm ^ y).normal(); + } + else if (dot < -0.999999999) { + // pre/next legs are pointing opposite directions + // so we're in a straight line + return -preLegNrm; + } + MVector bin = (preLegNrm ^ nextLegNrm).normal(); + return ((bin ^ preLegNrm) + (bin ^ nextLegNrm)).normal(); +} + +void buildSmoothMats(std::vector &dat, double startTension, double endTension, bool closed) +{ + for (size_t i = 1; i + 1 < dat.size(); ++i) { + auto &cur = dat[i]; + MVector tan = buildVertMatTangent( + cur.outTan.legLen, cur.inTan.legLen, cur.outTan.normLeg, cur.inTan.normLeg + ); + cur.smoothMat = buildMat(cur.tfm, cur.norm, tan); + + double preLegLen = cur.inTan.legLen * cur.inTan.weight / 3; + double nextLegLen = cur.outTan.legLen * cur.outTan.weight / 3; + cur.inTan.smoothTan = -tan * preLegLen; + cur.outTan.smoothTan = tan * nextLegLen; + } + + if (closed){ + auto &start = dat[0]; + auto &end = dat[dat.size() - 1]; + + MVector tan = buildVertMatTangent( + start.outTan.legLen, end.inTan.legLen, start.outTan.normLeg, end.inTan.normLeg + ); + start.smoothMat = buildMat(start.tfm, start.norm, tan); + end.smoothMat = start.smoothMat; + double preLegLen = end.inTan.legLen * end.inTan.weight / 3; + double nextLegLen = start.outTan.legLen * start.outTan.weight / 3; + + end.inTan.smoothTan = -tan * preLegLen; + start.outTan.smoothTan = tan * nextLegLen; + } + else{ + dat[0].outTan.smoothTan = (dat[1].tfm + dat[1].inTan.smoothTan * startTension - dat[0].tfm) * + (dat[0].outTan.weight / 2); + dat[0].smoothMat = buildMat(dat[0].tfm, dat[0].norm, dat[0].outTan.smoothTan.normal()); + + size_t s = dat.size(); + dat[s - 1].inTan.smoothTan = + (dat[s - 2].tfm + dat[s - 2].outTan.smoothTan * startTension - dat[s - 1].tfm) * + (dat[s - 1].inTan.weight / 2); + + dat[s - 1].smoothMat = buildMat(dat[s - 1].tfm, dat[s - 1].norm, -dat[s - 1].inTan.smoothTan.normal()); + } +} + +void buildLinearTangents(std::vector &dat) +{ + for (size_t i = 1; i + 1 < dat.size(); ++i) { + auto &prev = dat[i - 1]; + auto &cur = dat[i]; + auto &next = dat[i + 1]; + + double preLegLen = cur.inTan.legLen * cur.inTan.weight / 3; + double nextLegLen = cur.outTan.legLen * cur.outTan.weight / 3; + + MVector inDir = (prev.tfm + (prev.outTan.smoothTan * prev.outTan.smooth) - cur.tfm).normal(); + MVector outDir = (next.tfm + (next.inTan.smoothTan * next.inTan.smooth) - cur.tfm).normal(); + + cur.inTan.linearTan = inDir * preLegLen; + cur.outTan.linearTan = outDir * nextLegLen; + } + + size_t s = dat.size(); + + dat[0].outTan.linearTan = (dat[1].tfm + (dat[1].inTan.smoothTan * dat[1].inTan.smooth) - dat[0].tfm) / (3 - dat[1].inTan.smooth); + dat[s - 1].inTan.linearTan = (dat[s - 2].tfm + (dat[s - 2].outTan.smoothTan * dat[s - 2].outTan.smooth) - dat[s - 1].tfm) / (3 - dat[s-2].outTan.smooth); +} + +void buildDoneTangents(std::vector &dat) +{ + for (auto &cur : dat) { + MMatrix ti = cur.tfmMat.inverse(); + + double inSmo = cur.inTan.smooth; + double inAutoMul = cur.inTan.autoVal; + MMatrix inUserMat = ti * cur.inTan.userMat; + MVector inUserTan = MVector(inUserMat(3, 0), inUserMat(3, 1), inUserMat(3, 2)); + MVector inAutoTan = cur.inTan.smoothTan * inSmo + cur.inTan.linearTan * (1 - inSmo); + cur.inTan.doneTan = inAutoTan * inAutoMul + inUserTan * (1 - inAutoMul); + + double outSmo = cur.outTan.smooth; + double outAutoMul = cur.outTan.autoVal; + MMatrix outUserMat = ti * cur.outTan.userMat; + MVector outUserTan = MVector(outUserMat(3, 0), outUserMat(3, 1), outUserMat(3, 2)); + MVector outAutoTan = cur.outTan.smoothTan * outSmo + cur.outTan.linearTan * (1 - outSmo); + cur.outTan.doneTan = outAutoTan * outAutoMul + outUserTan * (1 - outAutoMul); + } +} + +MStatus TwistMultiTangentNode::compute(const MPlug &plug, MDataBlock &data) +{ + if (plug != aInTanX && plug != aInTanY && plug != aInTanZ && plug != aInTan && + plug != aOutTanX && plug != aOutTanY && plug != aOutTanZ && plug != aOutTan && + plug != aInTanLen && plug != aOutTanLen && plug != aOutTwistUpX && plug != aOutTwistUpY && + plug != aOutTwistUpZ && plug != aOutTwistUp && plug != aOutTwistMat && plug != aTangents) { + return MS::kUnknownParameter; + } + + // Read everything + MArrayDataHandle vertInputs = data.inputArrayValue(aVertData); + unsigned int icount = vertInputs.elementCount(); + + double startTension = data.inputValue(aStartTension).asDouble(); + double endTension = data.inputValue(aEndTension).asDouble(); + int maxVertices = data.inputValue(aMaxVertices).asInt(); + bool closed = data.inputValue(aClosed).asBool(); + + icount = (icount < maxVertices) ? icount : (unsigned int)maxVertices; + if (icount < 2) { + data.setClean(aInTan); + data.setClean(aInTanLen); + data.setClean(aInTanX); + data.setClean(aInTanY); + data.setClean(aInTanZ); + data.setClean(aOutTan); + data.setClean(aOutTanLen); + data.setClean(aOutTanX); + data.setClean(aOutTanY); + data.setClean(aOutTanZ); + data.setClean(aOutTwistMat); + data.setClean(aOutTwistUp); + data.setClean(aOutTwistUpX); + data.setClean(aOutTwistUpY); + data.setClean(aOutTwistUpZ); + data.setClean(aTangents); + + return MS::kSuccess; + } + + // Don't care which plug I'm being asked for. Just compute all of 'em + std::vector tanData; + + for (size_t i = 0; i < icount; ++i) { + TanData dat; + + vertInputs.jumpToArrayElement(i); + MDataHandle inHandle = vertInputs.inputValue(); + + dat.inTan.weight = inHandle.child(aInTanWeight).asDouble(); + dat.outTan.weight = inHandle.child(aOutTanWeight).asDouble(); + + dat.inTan.smooth = inHandle.child(aInSmooth).asDouble(); + dat.outTan.smooth = inHandle.child(aOutSmooth).asDouble(); + + dat.inTan.autoVal = inHandle.child(aInTanAuto).asDouble(); + dat.outTan.autoVal = inHandle.child(aOutTanAuto).asDouble(); + + MMatrix vmat = inHandle.child(aVertMat).asMatrix(); + dat.tfmMat = vmat; + dat.tfm = MVector(vmat(3, 0), vmat(3, 1), vmat(3, 2)); + dat.norm = MVector(vmat(1, 0), vmat(1, 1), vmat(1, 2)); + + dat.inTan.piMat = inHandle.child(aInParentInv).asMatrix(); + dat.outTan.piMat = inHandle.child(aOutParentInv).asMatrix(); + dat.piTwist = inHandle.child(aTwistParentInv).asMatrix(); + + dat.inTan.userMat = inHandle.child(aInTanMat).asMatrix(); + dat.outTan.userMat = inHandle.child(aOutTanMat).asMatrix(); + tanData.push_back(std::move(dat)); + } + + if (closed){ + auto &start = tanData[0]; + auto &end = tanData[tanData.size() - 1]; + start.inTan = end.inTan; + end.outTan = start.outTan; + } + + for (size_t i = 1; i < tanData.size(); ++i) { + auto &prev = tanData[i - 1]; + auto &cur = tanData[i]; + + cur.inTan.leg = prev.tfm - cur.tfm; + cur.inTan.legLen = cur.inTan.leg.length(); + cur.inTan.normLeg = cur.inTan.leg / cur.inTan.legLen; + + prev.outTan.leg = -cur.inTan.leg; + prev.outTan.legLen = cur.inTan.legLen; + prev.outTan.normLeg = -cur.inTan.normLeg; + } + + tanData[0].inTan.legLen = 0.0; + tanData[tanData.size() - 1].outTan.legLen = 0.0; + + buildSmoothMats(tanData, startTension, endTension, closed); + buildLinearTangents(tanData); + buildDoneTangents(tanData); + + auto outputs = data.outputArrayValue(aTangents); + auto builder = outputs.builder(); + for (size_t i = 0; i < tanData.size(); ++i) { + auto outHandle = builder.addElement(i); + auto &cur = tanData[i]; + + auto inTanHandle = outHandle.child(aInTan); + auto inPt = MPoint(cur.inTan.doneTan + cur.tfm) * cur.inTan.piMat; + inTanHandle.set3Double(inPt[0], inPt[1], inPt[2]); + + auto inTanLenHandle = outHandle.child(aInTanLen); + inTanLenHandle.setDouble(cur.inTan.doneTan.length()); + + auto outTanHandle = outHandle.child(aOutTan); + auto outPt = MPoint(cur.outTan.doneTan + cur.tfm) * cur.outTan.piMat; + outTanHandle.set3Double(outPt[0], outPt[1], outPt[2]); + + auto outTanLenHandle = outHandle.child(aOutTanLen); + outTanLenHandle.setDouble(cur.outTan.doneTan.length()); + + auto outMatHandle = outHandle.child(aOutTwistMat); + outMatHandle.setMMatrix(cur.smoothMat * cur.piTwist); + + auto outTwistHandle = outHandle.child(aOutTwistUp); + outTwistHandle.set3Double(cur.tfmMat(1, 0), cur.tfmMat(1, 1), cur.tfmMat(1, 2)); + } + + data.setClean(aInTan); + data.setClean(aInTanLen); + data.setClean(aInTanX); + data.setClean(aInTanY); + data.setClean(aInTanZ); + data.setClean(aOutTan); + data.setClean(aOutTanLen); + data.setClean(aOutTanX); + data.setClean(aOutTanY); + data.setClean(aOutTanZ); + data.setClean(aOutTwistMat); + data.setClean(aOutTwistUp); + data.setClean(aOutTwistUpX); + data.setClean(aOutTwistUpY); + data.setClean(aOutTwistUpZ); + data.setClean(aTangents); + + return MS::kSuccess; +} diff --git a/src/twistMultiTangentNode.h b/src/twistMultiTangentNode.h new file mode 100644 index 0000000..0fd3451 --- /dev/null +++ b/src/twistMultiTangentNode.h @@ -0,0 +1,83 @@ +/* +MIT License + +Copyright (c) 2018 Blur Studio + +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. +*/ + +#pragma once + +#include +#include +#include +#include + +class TwistMultiTangentNode : public MPxNode { +public: + TwistMultiTangentNode(); + virtual ~TwistMultiTangentNode(); + + virtual MStatus compute( const MPlug& plug, MDataBlock& data ); + static void* creator(); + static MStatus initialize(); + +public: + // inputs + static MObject aVertMat; + static MObject aInTanMat; + static MObject aOutTanMat; + static MObject aInTanWeight; + static MObject aOutTanWeight; + static MObject aInTanAuto; + static MObject aOutTanAuto; + static MObject aInSmooth; + static MObject aOutSmooth; + static MObject aInParentInv; + static MObject aOutParentInv; + static MObject aTwistParentInv; + static MObject aVertData; + + static MObject aStartTension; + static MObject aEndTension; + static MObject aMaxVertices; + static MObject aClosed; + + // outputs + static MObject aInTanX; + static MObject aInTanY; + static MObject aInTanZ; + static MObject aInTan; + static MObject aOutTanX; + static MObject aOutTanY; + static MObject aOutTanZ; + static MObject aOutTan; + static MObject aInTanLen; + static MObject aOutTanLen; + static MObject aOutTwistUpX; + static MObject aOutTwistUpY; + static MObject aOutTwistUpZ; + static MObject aOutTwistUp; + static MObject aOutTwistMat; + static MObject aTangents; + + static MTypeId id; +}; + + diff --git a/src/twistSpline.h b/src/twistSpline.h index 9272511..bacf3e2 100644 --- a/src/twistSpline.h +++ b/src/twistSpline.h @@ -943,7 +943,7 @@ class TwistSpline { mat[0][0] = 0.0; mat[0][1] = -1.0; mat[0][2] = 1.0 - lv[0]; - res[0] = -lv[0] * rv[0]; + res[0] = lv[0] * rv[0]; // Mid Cases for (size_t i = 1; i < len - 1; ++i) { @@ -951,7 +951,7 @@ class TwistSpline { mat[i][0] = (1.0 - lv[i]) * (1.0 - A); mat[i][1] = -1.0; mat[i][2] = (1.0 - lv[i]) * A; - res[i] = -lv[i] * rv[i]; + res[i] = lv[i] * rv[i]; } // End case @@ -959,7 +959,7 @@ class TwistSpline { mat[e][0] = 1.0 - lv[e]; mat[e][1] = -1.0; mat[e][2] = 0.0; - res[e] = -lv[e] * rv[e]; + res[e] = lv[e] * rv[e]; // Then pass it to the solver solveTridiagonalMatrix(mat, res); @@ -1045,7 +1045,7 @@ class TwistSpline { for (size_t i=0; igetLength() + segLens[i]; //restVals[i+1] = segments[i]->postAngle() + restVals[i]; - orientVals[i + 1] = segments[i]->postAngle(); + orientVals[i + 1] = -segments[i]->postAngle(); } // Get the angle difference between the last diff --git a/src/twistSplineNode.cpp b/src/twistSplineNode.cpp index 1fb84da..275aa12 100644 --- a/src/twistSplineNode.cpp +++ b/src/twistSplineNode.cpp @@ -84,6 +84,7 @@ MObject TwistSplineNode::aSplineDisplay; MObject TwistSplineNode::aDebugDisplay; MObject TwistSplineNode::aDebugScale; MObject TwistSplineNode::aMaxVertices; +MObject TwistSplineNode::aTwistMultiplier; TwistSplineNode::TwistSplineNode() {} TwistSplineNode::~TwistSplineNode() {} @@ -126,8 +127,10 @@ MStatus TwistSplineNode::initialize() { aDebugScale = nAttr.create("debugScale", "ds", MFnNumericData::kDouble, 1.0); addAttribute(aDebugScale); aMaxVertices = nAttr.create("maxVertices", "mv", MFnNumericData::kInt, 1000); - nAttr.setMin(2); + nAttr.setMin(2); addAttribute(aMaxVertices); + aTwistMultiplier = nAttr.create("twistMultiplier", "tm", MFnNumericData::kDouble, -1.0); + addAttribute(aTwistMultiplier); //--------------- Array ------------------- @@ -169,44 +172,37 @@ MStatus TwistSplineNode::initialize() { addAttribute(aVertexData); - attributeAffects(aScaleCompensation, aOutputSpline); - attributeAffects(aInTangent, aOutputSpline); - attributeAffects(aControlVertex, aOutputSpline); - attributeAffects(aOutTangent, aOutputSpline); - attributeAffects(aParamValue, aOutputSpline); - attributeAffects(aParamWeight, aOutputSpline); - attributeAffects(aTwistWeight, aOutputSpline); - attributeAffects(aTwistValue, aOutputSpline); - attributeAffects(aUseOrient, aOutputSpline); - attributeAffects(aMaxVertices, aOutputSpline); - - attributeAffects(aScaleCompensation, aNurbsData); - attributeAffects(aInTangent, aNurbsData); - attributeAffects(aControlVertex, aNurbsData); - attributeAffects(aOutTangent, aNurbsData); - attributeAffects(aParamValue, aNurbsData); - attributeAffects(aParamWeight, aNurbsData); - attributeAffects(aTwistWeight, aNurbsData); - attributeAffects(aTwistValue, aNurbsData); - attributeAffects(aUseOrient, aNurbsData); - attributeAffects(aMaxVertices, aNurbsData); - - attributeAffects(aInTangent, aSplineLength); - attributeAffects(aControlVertex, aSplineLength); - attributeAffects(aOutTangent, aSplineLength); - attributeAffects(aMaxVertices, aSplineLength); - - // Geometry changing - attributeAffects(aScaleCompensation, aGeometryChanging); - attributeAffects(aInTangent, aGeometryChanging); - attributeAffects(aControlVertex, aGeometryChanging); - attributeAffects(aOutTangent, aGeometryChanging); - attributeAffects(aParamValue, aGeometryChanging); - attributeAffects(aParamWeight, aGeometryChanging); - attributeAffects(aTwistWeight, aGeometryChanging); - attributeAffects(aTwistValue, aGeometryChanging); - attributeAffects(aUseOrient, aGeometryChanging); - attributeAffects(aMaxVertices, aGeometryChanging); + std::vector ins, outs, lenup; + ins.push_back(&aScaleCompensation); + ins.push_back(&aInTangent); + ins.push_back(&aControlVertex); + ins.push_back(&aOutTangent); + ins.push_back(&aParamValue); + ins.push_back(&aParamWeight); + ins.push_back(&aTwistWeight); + ins.push_back(&aTwistValue); + ins.push_back(&aUseOrient); + ins.push_back(&aMaxVertices); + ins.push_back(&aTwistMultiplier); + + outs.push_back(&aOutputSpline); + outs.push_back(&aNurbsData); + outs.push_back(&aGeometryChanging); + + lenup.push_back(&aInTangent); + lenup.push_back(&aControlVertex); + lenup.push_back(&aOutTangent); + lenup.push_back(&aMaxVertices); + + for (auto inptr: ins){ + for (auto outptr: outs){ + attributeAffects(*inptr, *outptr); + } + } + + for (auto lenptr: lenup){ + attributeAffects(*lenptr, aSplineLength); + } return MS::kSuccess; } @@ -302,6 +298,9 @@ MStatus TwistSplineNode::compute(const MPlug& plug, MDataBlock& data) { MDataHandle scaleCompH = data.inputValue(aScaleCompensation); double scaleComp = scaleCompH.asDouble(); + MDataHandle hTwistMultiplier = data.inputValue(aTwistMultiplier); + double twistMultiplier = hTwistMultiplier.asDouble(); + // loop over the input matrices MArrayDataHandle inputs = data.inputArrayValue(aVertexData); unsigned ecount = inputs.elementCount(); @@ -327,7 +326,7 @@ MStatus TwistSplineNode::compute(const MPlug& plug, MDataBlock& data) { if (lockIt > 0.0) gotLocks = true; twistLock.push_back(group.child(aTwistWeight).asDouble()); - userTwist.push_back(group.child(aTwistValue).asDouble()); + userTwist.push_back(group.child(aTwistValue).asDouble() * twistMultiplier); double oriIt = group.child(aUseOrient).asDouble(); orientLock.push_back(oriIt); diff --git a/src/twistSplineNode.h b/src/twistSplineNode.h index d305d10..f698f4b 100644 --- a/src/twistSplineNode.h +++ b/src/twistSplineNode.h @@ -82,6 +82,7 @@ class TwistSplineNode : public MPxLocatorNode static MObject aOutputSpline; static MObject aSplineLength; static MObject aMaxVertices; + static MObject aTwistMultiplier; // NURBS curve output data static MObject aNurbsData;