Hello, it's been a while since I've posted anything here, but it's not that I've abandoned it, and I still have several ideas to share. On this occasion, I'm going to share a problem I've been having lately while working with rigs. Calculating the distance between two points is a frequently used operation, and we almost always have to compensate for scale, since animators tend to scale the controls. However, this usually only works if everything is scaled uniformly; otherwise, the distance calculation will be altered.
Wes' Stuff
TECHNICAL ARTIST - 3D RIGGING
Saturday, October 18, 2025
Calculate Distance Using Non-Uniform Scale
Wednesday, June 11, 2025
Curve Rivet v2
Hello there! Last year I was battling with this issue, I have always used ribbons for all my setups that require bendy bones. Once an animator told me something related with them. In some axis, on an extreme position, the ribbon breaks and sometimes we don't need to control the twist.
I've been looking for an answer, but I've been told to only use SplineIK or Motion Path or a custom node. Therefore, I wanted to do a simple setup to mimic the behaviour of the SplineIK without the issues that it has. Besides, I got interested in doing rigs setup with less DAG nodes. I came across with the way how Vasil Shotarov does the rivet on NURBS Surfaces without any DAG node.
I've been doing several tests related with this approach. I'm going to explain each method to use a curve to drive transforms, without using custom nodes, and explain its advantages and disadvantages.
SplineIK:
- Customizable a bit tricky ✔
- It has a built-in twist setup ✔
- It's required to know the primary and secondary axis ✖
- It's unstable when you stretch ✖
- Require lots of dag nodes and joints in order to work properly ✖
- Can keep the same distance between transforms ✔
- Very heavy for parallel evaluation ✖✖
MotionPath:
- Very simple and straightforward ✔
- It needs a separate twist setup ✖
- It's required to know the primary and secondary axis ✖
- It's stable when you stretch it ✔
- It has issues related with flipping in the up axis✖
- It doesn't require any dag nodes, except for the worldUpObject ✔
- Can keep the same distance between transforms ✔
- Light for parallel evaluation ✔
IkHandle + PointOnCurveInfo + Extend Curve:
- Very tricky setup ✖✖
- It needs a separate twist setup ✖
- Doesn't required to know any axis ✔✔
- It's stable when you stretch it ✔
- Sometimes it will calculate the rotation incorrectly, I haven't tested it enough ✖
- Require lots of dag nodes and joints in order to work properly ✖
- Cannot keep the same distance between transforms ✖
- Heavy for parallel evaluation ✖
As you can see here each method has its differences, you will need to outweigh each method and use one of them depending on your necessities. However, I recently discovered a way to solve the flipping, instability and it don't require a bunch of dag nodes. This method I'm going to call it Curve Rivet. I'm going to explain how to set it up.
Curve Rivet:
- Tricky setup ✖
- It needs a separate twist setup ✖
- It's stable when you stretch it ✔
- Doesn't have any issues related with flipping ✔✔
- It doesn't require any dag nodes, except for the worldUpObject ✔
- Can keep the same distance between transforms ✔
- Light for parallel evaluation ✔
I'm showing the graph of the setup, which is quite straightforward. There are a few things to take into account:
- The curve driver must be normalized.
- The orientation of the worldUpObj doesn't matter, because it's been compensated.
- The final composeMatrix should use quaternions not euler.
- As it's shown, we need to know the aimOffset. There are several ways to obtain them. I'll show you the method that I use:
import maya.api.OpenMaya as om2 import maya.cmds as mc parent = 'C_tailJ_chains_group_worldSpace_decomMatrix' child = 'C_tailJ_1_ctrl_Pre_eulerToQuat' quatParent = om2.MQuaternion(mc.getAttr(parent+'.outputQuat')[0])
quatChild = om2.MQuaternion(mc.getAttr(child+'.outputQuat')[0])
aimOffset = quatChild*quatParent.inverse()
- If you want to make the setup even more stable, since the tips may flip, you can change the worldUpObject to a previous chain. This I called chainedWorldUp
As you can see is working fine, but you may know that the PoCI (pointOnCurveInfo) cannot keep the same distance between the transforms while we move the curve. We can use a motionPath instead of it. This node has a build-in attribute called "Parametric length", which will do what the PoCI can't. The only drawback of the motionPath is the tangent vector is not obtainable from it. For this reason, I will explain the following:
Fake Tangents:
As I mentioned before we can use a motionPath, but it's mandatory to have a tangent vector to create the rivet, so in this case I am going to use what I called "Fake Tangents" which is basically a point which is very close to the base point. We can make a small script to keep the U Value of the Fake Tangent between 0 to 1.
threshold = 0.05 uValueFakeTan = uValue + threshold if uValueFakeTan > 1:
uValueFakeTan = uValue - threshold
- 201 joints.
- 5 controls, randomly animated.
- A curve or surface depending on the case.
- A transform used as worldUpObj.
- Used the matrix constraint.
- The cycle clusters were removed.
Method |
FPS |
Bare Motion Path |
100 |
Matrix Rivet |
96 |
Curve Rivet |
90 |
Curve Rivet + Fake Tangents |
69 |
Spline IK |
41 |
Thursday, July 18, 2024
Convert ngSkinTools file 1to2
Hi, I wanted to write something small and quite useful. I have been pretty steady using ngSkinTools v1, but it is already a discontinued version.
I'm going to share a small script to convert all the .ma files that are in the first version to the second version. This works for folders and subfolders. According to the internal docs, this only works if you have both versions installed. For this example I will use Maya 2020, which was the last version.
The code:
This won't override the files, it will create a folder where the conversion will be saved in another .ma file. I hope you find this useful!
Tuesday, January 16, 2024
Geo Connector Rivet
- Create a set for a single or a group of vertices, if it's a group the rivet position will be averaged between them.
- We create a locator and a geoConnector node.
- Now, we need to create a vectorArray attribute to the geoConnector to hold a connection. We can called whatever we want.
mc.addAttr(geoCon,ln='ComponentPositionStatic',dt='vectorArray')
- We need to find out the groupID, which was created when we create the set. You can localized in the node editor. Remember to turn on the visualization of the auxiliary nodes.
- However, we have in the outliner a set without any function. Moreover, this depend on the number of rivets that we have in scene. Well, here is the solution for that. Just disconnect and delete in that order.
- If we made everything as it was explained, the rivet will keep working.
- We need to do this after applying all the deformation to our mesh. If we create a deformer after the rivet was created, it won't work when you reopen the scene.
- This method is not parallel friendly, because of the geoConnector. Use it wisely.
Tuesday, January 2, 2024
Set Default Tool
Hey there! I am back. First of all. Happy New Year! I hope to keep posting things.
This post will be brief. Some times we need to set a default values for float or integer attributes on the channel box. Maya doesn't have a tool to do that, you need to do it by cmds. This is particularly useful when the default value is not 0. We can use the next command to query the default value, to reset the attribute.
dValue = mc.addAttr(source + '.' + attr, q=1,dv=1) mc.setAttr(source + '.' +attr, dValue)
So, I built a simple tool for that. You just need to select a custom attribute float or integer and press the button. The value won't change, but internally the default value will.
The code:
I hope you find this helpful. Thanks
Friday, December 29, 2023
Local Rigging vol. 2
This post is the continuation of the previous post, please check it before read this one.
Relative World:
The main idea of this setup is allow you to create groups above the controls and the joints will keep following the controls, but they are not going to follow the controls if they are moved from a certain group above them. I'm going to name each component: the control (source), joint (target), a group above the control (top parent). The top parent is not necessary to be an immediate parent of control. Some notes to take into account:
- The offset is the offset between the source and the top parent.
- In this case I'm not using the parentInvertMatrix of the target, because I don't want to create more cycles for parallel evaluation. Instead I'm turning off the inheritsTransform.
So if everything is done correctly, all the controls will work properly and the groups above them will also work correctly. However if you move the top parent, the joints won't move at all.
This is an alternative method for local rigging. I hope you find it useful.
Sunday, December 10, 2023
Local Rigging vol. 1
Recently, I took the course of Optimizing Rigs for Parallel Evaluation on Rigging Dojo by Charles Wardlaw, a very good course by the way. One of the topics that he explains is the local rig. When I started doing rigging years ago ( I feel old now D: ) I made local rigs for many parts of the face without any control, for the eyebrows, for eyelids, for lips, for mouth, for everything. The main problems that I faced was the normalization of the skinCluster and if you don't have a good control of that, it can be a nightmare to edit the rig. Moreover, when you want to move a control and at the same time the joint, you will need to duplicate the setup on the local rig. Since, I built my own autorig system I tried to avoid that, but the main problem of having the whole setup on a single chain is the performance. Obviously, if you want to export the skeleton to a game engine is mandatory to have the whole system on a single chain, but for films is not required.
I bumped up with the idea of using locals rigs without using direct connections from controls to joints. I discovered two methods that you can use to approach this setup. I divided this into two posts
Reciprocal Driving:
This setup is a bit complex to make, but if you understand it, you will get good results. First of all, you need to make the setup (groups, parents, constrains, etc.) on the local rigging group not in the global rigging. In the global rigging, you will only put the controls and a couple of groups above them. First I will name the transforms that are involved:
- controls = animatable controls
- relative controls = controls on the local rigging
- buffers = group above the animatable controls
The connections will be like this:
1. Direct connection from the control to the relative controls (T,R,S)
2. We will use the parent matrix of the relative controls and we will multiply by itself but static and inverted. The result should be an identity matrix, then we will decompensate the matrix and connect to buffers or use the offsetParentMatrix.
VoilĂ ! You now have a local rigging setup if you want to create more groups above controls, maybe to control them, just create them above the relative controls. The only downside that I found is this only will work if the all the controls are not parented between them.
In the next post I will explain the second method, which is very different from this one. See you there and happy rigging!




























