← 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
- Click on the FFD
- 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()