Ben Traje
← Back to cinema4d

Create Controls from FFD Points in Cinema 4D

01 May 26 (5mo ago)

I like FFDs (Lattice Deformers). They save me countless hours when rigging non-spherical eyes, handling squash-and-stretch setups, creating pose-specific corrections (though the camera deformer is more direct for screen-space tweaks), and shaping stylized characters.

The classic headache is that Cinema 4D doesn't let you directly manipulate or animate an FFD deformer's individual lattice points outside of point edit mode. This script solves that limitation by generating dedicated Null objects positioned and oriented directly at each lattice point, making it effortless to grab, animate, or constrain controls in object mode.

What's New in This Version

  • Child Selection Support: Switched to GETACTIVEOBJECTFLAGS_CHILDREN so selected hierarchies containing multiple FFDs are handled smoothly.
  • Matrix & Orientation Alignment: Nulls now inherit the full world matrix, orientation axes, and local parent offsets of the FFD deformer.
  • Scoped Naming: Point Nulls and parent groups are prefixed with the deformer name (e.g., FFD_FFD_Nulls and FFD_P00) to avoid scene clutter and namespace collisions.
  • Robust Feedback: Clearer dialogue warnings and status messages alert you whether an FFD was detected and how many handles were generated.

How to Use

  1. Select your FFD deformer (or parent group containing FFDs).
  2. Run the script.
  3. The script creates an aligned parent group containing individual nulls at every lattice point.

Note on updating: The script is non-procedural. If you change the grid resolution of your FFD, clear out the generated null group and rerun the script to produce an updated set of control handles.

import c4d
from c4d import gui

def create_null_at_position(doc, pos, name, radius=10.0, parent=None, orient_mg=None):
    null = c4d.BaseObject(c4d.Onull)
    null.SetName(name)
    
    try:
        null[c4d.NULLOBJECT_RADIUS] = radius
    except Exception:
        pass

    # Initial matrix
    m = c4d.Matrix()
    if orient_mg:
        m.v1, m.v2, m.v3 = orient_mg.v1, orient_mg.v2, orient_mg.v3
    m.off = pos

    null.SetMg(m)

    # Insert under parent while maintaining proper local offset
    if parent:
        local_m = ~parent.GetMg() * m
        doc.InsertObject(null, parent=parent, checknames=True)
        null.SetMl(local_m)
    else:
        doc.InsertObject(null, checknames=True)

    return null

def build_group_parent(doc, ffd):
    grp = c4d.BaseObject(c4d.Onull)
    grp.SetName(f"{ffd.GetName()}_FFD_Nulls")
    grp.SetMg(ffd.GetMg())
    doc.InsertObject(grp, parent=None, checknames=True)
    return grp

def ffd_to_point_nulls(doc, ffd, null_radius=10.0):
    if ffd is None or ffd.GetType() != c4d.Offd:
        return 0

    pts_local = ffd.GetAllPoints()
    if not pts_local:
        return 0

    mg = ffd.GetMg() # Full world matrix of FFD (orientation + scale + pos)
    grp = build_group_parent(doc, ffd)

    created = 0
    for i, p in enumerate(pts_local):
        pw = mg * p # Position in world coordinates
        name = f"{ffd.GetName()}_P{str(i).zfill(2)}"

        n = create_null_at_position(doc, pw, name, radius=null_radius, parent=grp)
        if n:
            # Copy rotation/orientation from FFD
            nm = n.GetMg()
            nm.v1 = mg.v1 # X axis
            nm.v2 = mg.v2 # Y axis
            nm.v3 = mg.v3 # Z axis
            n.SetMg(nm)
            created += 1

    return created

def main():
    doc.StartUndo()
    try:
        selection = doc.GetActiveObjects(c4d.GETACTIVEOBJECTFLAGS_CHILDREN)
        if not selection:
            gui.MessageDialog("Select at least one FFD deformer.")
            return

        total_created = 0
        for obj in selection:
            created = ffd_to_point_nulls(doc, obj, null_radius=12.0)
            total_created += created

        if total_created == 0:
            gui.MessageDialog("No nulls created. Make sure you selected an FFD deformer.")
        else:
            c4d.StatusSetText(f"Created {total_created} null(s).")
    finally:
        doc.EndUndo()
        c4d.EventAdd()

if __name__ == "__main__":
    main()