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_CHILDRENso 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_NullsandFFD_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
- Select your FFD deformer (or parent group containing FFDs).
- Run the script.
- 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()