Home Website Youtube GitHub

Joint-Based Skin Deformation

I’ve recently been rigging muscular characters, which has gotten me interested in researching muscle and skeletal systems.

I came across this article: Joint-Based Skin Deformation in Ragnarök Will Be Explained at GDC 2023
But I haven’t been able to find similar tutorials or articles, so I wanted to ask if anyone knows of any resources on this topic.

Currently I’m using RBF combined with manually-placed support joints, but I’m not satisfied with the results. I’ve also considered SSDR, but it doesn’t conform to a max influence of 4 joints — sometimes it assigns more, which makes the weights difficult to control.

Are there any other approaches I could try?

Ideally I’m looking for Joint-Based Deformation techniques suitable for Unity. Since Unity has a max influence limit of 4 joints, I want to minimize the use of blendshapes and solve deformation with joints instead.
▼God of War Ragnarök


▲God of War Ragnarök

2 things: in mGear, look into gimmick joints, and make a post run to rebuild them after every iteration. These are good for twisting.

Where you’ll really get some interesting stuff is using ribbons. General idea is to turn a nurbs plane into an nHair deformer, and parent the joints under the follicles. Then use those joints to drive the deformer joints. With this set up, you can use any of the nonlinear deformers on a seperate nurbs plane ribbon, make that ribbon a blend shape for the main ribbbon, and move the follicle joints, hence the deform joints, with it. No blend shapes on the export mesh. All joint driven.

You can always connect nodes outside the hierarchy to drive the deform joints. For example, make control0 at the elbow or something and have its rotation drive a deform joint in the bicep using math nodes, remap values, clamps, etc.

Unfortunately all of this is done post run (as far as I know at the moment) so you’ll have to learn a little python and dig into the source code.

Here are some old scripts I made that drive joints from other objects. They’re hard coded, so easy to use.

Remap values (dont use too many, theyre heavy)

def remap_driver(driver, driven, driven_attr, driver_attr, inMin, inMax, outMin, outMax):
        remapper = cmds.createNode("remapValue", n=f"{driver}_{driver_attr}_remap")
        cmds.setAttr(f"{remapper}.inputMin", inMin)     
        cmds.setAttr(f"{remapper}.inputMax", inMax)   
        cmds.setAttr(f"{remapper}.outputMin", outMin)
        cmds.setAttr(f"{remapper}.outputMax", outMax)
        cmds.connectAttr(f"{driver}.{driver_attr}", f"{remapper}.inputValue")
        cmds.connectAttr(f"{remapper}.outValue", f"{driven}.{driven_attr}")

        return remapper

Get the average rotation between 2 objects, and then apply that average to another node (essentially gimmick joints, but with clamps)

def average_rot_between(input1, input2, output, host, axis):
    avg = cmds.createNode("plusMinusAverage", n = f"{output}_avgRot")
    cmds.setAttr(f"{avg}.operation", 3)

    base = "_".join(output.split("_")[:1])
    clamp_min = f"{base}_clamp_min"
    if not cmds.attributeQuery(clamp_min, n = host, exists = True):
        cmds.addAttr(host, ln=clamp_min, at = "float", min = -180, max = 180, dv = 0, k = True)
    clamp_max = f"{base}_clamp_max"
    if not cmds.attributeQuery(clamp_max, n = host, exists = True):
        cmds.addAttr(host, ln = clamp_max, at = "float", min = -180, max = 180, dv = 0, k = True)

    for i, inp in enumerate([input1, input2]):
        clamp = cmds.createNode("clamp", n = f"{inp}_avgRot_clamp")
        cmds.setAttr(f"{clamp}.minR", -360)
        cmds.setAttr(f"{clamp}.maxR", 360)
        cmds.connectAttr(f"{inp}.rotate{axis}", f"{clamp}.inputR")
        cmds.connectAttr(f"{host}.{clamp_min}", f"{clamp}.minR")
        cmds.connectAttr(f"{host}.{clamp_max}", f"{clamp}.maxR")
        cmds.connectAttr(f"{clamp}.outputR", f"{avg}.input1D[{i}]")

    mult = cmds.createNode("multiplyDivide", n = f"{output}_avgRot_switch")
    cmds.connectAttr(f"{avg}.output1D", f"{mult}.input1X")

    switch_attr = f"{base}_avgRot{axis}_switch"
    if not cmds.attributeQuery(switch_attr, n = host, exists = True):
        cmds.addAttr(host, ln = switch_attr, at = "float", min = 0, max = 2, dv = 1, k = True)

    cmds.connectAttr(f"{host}.{switch_attr}", f"{mult}.input2X")
    cmds.connectAttr(f"{mult}.outputX", f"{output}.rotate{axis}")

I’m not a very good coder-- I’m self taught so im sure this sucks. But it works for me.

hope this helps

1 Like