Ben Traje
← Back to cinema4d

Create Controls from FFD Points in Cinema4D

01 May 26 (5d ago)

I like FFD or Lattice Deformer. They save me a lot in rigging nonspherical eyes, squash and stretch, pose specific corrections (although the camera deformer is more direct) and even in modelling stylized characters.

The only problem is you can't directly control the points of an FFD deformer.So this script improves upon it. Rather than going in and out of edit mode, you can just manipulate the null controls. And you can better animate the points/controls, if you have to.

How to Use

  1. Click on the FFD
  1. Execute Script

Updating FFD? Unfortunately, the script is not procedural. So you have to delete the controls + python tag then rerun the script if you want a new and updated set of controls.

# Exports to the directory of the current C4D Document

import c4d
from c4d import gui

def get_tag_code(userdata_id):
    return f"""import c4d
def main():
    obj = op.GetObject()
    try:
        target_grp = obj[c4d.ID_USERDATA, {userdata_id}]
    except:
        return
        
    if not target_grp:
        return

    nulls = target_grp.GetChildren()
    pts = obj.GetAllPoints()
    
    if len(nulls) != len(pts):
        return

    inv_mg = ~obj.GetMg()
    # Pulling from GetMg ensures the world position is accurate 
    # regardless of whether the coordinates are frozen or not.
    new_pts = [inv_mg * n.GetMg().off for n in nulls]
    
    obj.SetAllPoints(new_pts)
    obj.Message(c4d.MSG_UPDATE)
"""

def create_user_data_link(obj, name):
    bc = c4d.GetCustomDatatypeDefault(c4d.DTYPE_BASELISTLINK)
    bc[c4d.DESC_NAME] = name
    bc[c4d.DESC_SHORT_NAME] = name
    bc[c4d.DESC_ANIMATE] = c4d.DESC_ANIMATE_OFF
    descid = obj.AddUserData(bc)
    return descid

def ffd_to_controlled_nulls(doc, ffd):
    if ffd.GetType() != c4d.Offd:
        return 0
    
    pts_local = ffd.GetAllPoints()
    mg = ffd.GetMg()
    grp = c4d.BaseObject(c4d.Onull)
    grp.SetName(f"{ffd.GetName()}_Controls")
    grp.SetMg(mg)
    doc.InsertObject(grp)

    red_color = c4d.Vector(1, 0, 0)

    for i, p in enumerate(pts_local):
        null = c4d.BaseObject(c4d.Onull)
        null.SetName(f"P{str(i).zfill(2)}")
        
        # --- Object Properties ---
        null[c4d.NULLOBJECT_DISPLAY] = 11 # Cube
        null[c4d.NULLOBJECT_RADIUS] = 12.0
        null[c4d.NULLOBJECT_ORIENTATION] = 3 # Y orientation
        
        # --- Basic Properties ---
        null[c4d.ID_BASEOBJECT_USECOLOR] = 2 # Always
        null[c4d.ID_BASEOBJECT_COLOR] = red_color
        
        # 1. Set the initial position in world space
        null.SetMg(mg * c4d.Matrix(off=p))
        
        # 2. Transfer from REL to FROZEN (The "Freeze All" logic)
        # We capture the relative vector before zeroing it out
        rel_pos = null[c4d.ID_BASEOBJECT_REL_POSITION]
        rel_rot = null[c4d.ID_BASEOBJECT_REL_ROTATION]
        rel_scale = null[c4d.ID_BASEOBJECT_REL_SCALE]
        
        # Set Frozen values
        null[c4d.ID_BASEOBJECT_FROZEN_POSITION] = rel_pos
        null[c4d.ID_BASEOBJECT_FROZEN_ROTATION] = rel_rot
        null[c4d.ID_BASEOBJECT_FROZEN_SCALE] = rel_scale
        
        # 3. Zero out the PSR (Relative values)
        null[c4d.ID_BASEOBJECT_REL_POSITION] = c4d.Vector(0)
        null[c4d.ID_BASEOBJECT_REL_ROTATION] = c4d.Vector(0)
        null[c4d.ID_BASEOBJECT_REL_SCALE] = c4d.Vector(1)
        
        doc.InsertObject(null, parent=grp)

    # User Data and Tag Setup
    doc.AddUndo(c4d.UNDOTYPE_CHANGE, ffd)
    ud_descid = create_user_data_link(ffd, "Control Nulls")
    ffd[ud_descid] = grp
    
    tag = c4d.BaseTag(c4d.Tpython)
    tag.SetName("FFD_Point_Linker")
    ud_id = ud_descid[1].id
    tag[c4d.TPYTHON_CODE] = get_tag_code(ud_id)
    ffd.InsertTag(tag)
    
    return len(pts_local)

def main():
    doc.StartUndo()
    try:
        selection = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_0)
        if not selection:
            gui.MessageDialog("Select an FFD.")
            return

        for obj in selection:
            count = ffd_to_controlled_nulls(doc, obj)
            if count > 0:
                c4d.StatusSetText(f"Linked {count} points with Red Cubes & Frozen PSR.")
    finally:
        doc.EndUndo()
        c4d.EventAdd()

if __name__ == "__main__":
    main()