π Blender Python API#
Everything the add-on does from the UI (creating groups, pinning vertex groups, keyframing spins, dropping invisible colliders, snapping meshes) can be driven from Python inside Blenderβs scripting editor. This is the right tool for procedural scene setup, batch variant generation, regression tests, and anything you do not want to click through three hundred times.
Tip
This page is a tutorial-style walkthrough. For the full method-by-method list, generated directly from the source, see the Blender Python API Reference.
Import#
from bl_ext.user_default.ppf_contact_solver.ops.api import solver
The add-on registers under Blenderβs extension namespace, so this import works regardless of where the add-on lives on disk. Every example below assumes it is already imported.
Scene Parameters#
solver.param is a whitelisted proxy over the scene-level state. Set
any exposed property by attribute:
solver.param.project_name = "shirt_drape"
solver.param.frame_count = 180
solver.param.frame_rate = 60
solver.param.step_size = 0.001
solver.param.gravity = (0, 0, -9.8) # alias for gravity_3d
solver.param.air_density = 0.001225
gravity is an alias for gravity_3d; reads and writes go through it
transparently. See Scene Parameters for
the full list.
Dynamic Parameters#
Note
dyn() is the legacy path. It writes the add-onβs own keyframe list,
which is converted into ordinary F-curves and emptied the next time the
.blend is loaded β the animation survives, the list does not, so a
script that reads state.dyn_params back after a reload finds it empty.
New scripts should keyframe the scene property directly, e.g.
state.keyframe_insert(data_path="gravity_3d", frame=60). Note the
property name: gravity is a solver.param alias, not a data path. See
Dynamic Parameters.
The builder is kept for existing scripts. The API mirrors the frontendβs
session.param.dyn() but takes frames, not seconds:
# Flip gravity at frame 60: hold the initial value through 60,
# then snap to the new value at 61.
solver.param.dyn("gravity").time(60).hold().time(61).change((0, 0, 9.8))
# Start a wind gust at frame 30.
solver.param.dyn("wind").time(30).hold().time(31).change((0, 1, 0), strength=5.0)
# Scalars like air_density just take a float.
solver.param.dyn("air_density").time(100).change(0.005)
# Nuke a dynamic param entirely.
solver.param.dyn("gravity").clear()
clear() empties the legacy list only, so once a file has been reloaded
and converted it removes nothing; deleting the F-curve is a Blender
operation.
Valid keys: "gravity", "wind", "air_density", "air_friction",
"vertex_air_damp". Frames must be strictly increasing within a chain;
time(30).time(30) raises.
See Dynamic Parameters for the
semantics of hold() vs. change().
Groups#
# type is one of: SOLID | SHELL | ROD | STATIC | PDRD | SAND
cloth = solver.create_group("Cloth", type="SHELL")
cloth.add("Shirt", "Pants")
cloth.param.shell_density = 0.5
cloth.param.shell_young_modulus = 50.0
cloth.param.friction = 0.3
cloth.param.bend = 0.5
cloth.set_overlay_color(0.0, 0.75, 0.0, 0.75) # rgba in [0, 1]
body = solver.create_group("Body", type="STATIC")
body.add("Mannequin")
create_group returns a group proxy. Look one up later by UUID with
solver.get_group(uuid), or walk every active group:
for g in solver.get_groups():
print(g.uuid, g.param.friction)
Group Surface#
Method |
Purpose |
|---|---|
|
Add one or more mesh objects by name |
|
Remove one object |
|
Set and enable the viewport overlay color |
|
Pin a vertex group; returns a pin proxy (see below) |
|
List every pin in this group as pin proxies |
|
Remove this group |
|
Whitelisted material/contact parameter access |
|
UUID string, stable across renames |
Material parameters on .param are validated: assigning a name outside
the whitelist raises AttributeError. See
Material Parameters for the full
list.
Bulk lifecycle operations live on the solver:
solver.delete_all_groups()
solver.clear() # full reset: groups, scene params, merge pairs,
# colliders, dyn params, fetched animation
Pins and Operations#
group.create_pin(object_name, vertex_group_name) returns a pin proxy.
Every mutating method returns self, so chaining works:
pin = cloth.create_pin("Shirt", "ShoulderPins")
pin.spin(axis=(0, 0, 1), angular_velocity=360, frame_start=1, frame_end=60)
pin.unpin(frame=90)
# Soft pin instead of a hard constraint.
cloth.create_pin("Shirt", "HemPins").pull(strength=2.0)
# Chain a scale and a spin on the same pin.
(cloth.create_pin("Shirt", "HemPins")
.scale(factor=0.5, center_direction=(0, 0, -1), frame_start=1, frame_end=60)
.spin(axis=(0, 1, 0), angular_velocity=180, center_vertex=42))
Pin Surface#
Method |
Purpose |
|---|---|
|
Switch to a soft pull force |
|
Ramp a translation over a frame range |
|
Rotate about a derived pivot |
|
Scale from a derived pivot |
|
PCA-axis torque |
|
Release the pin after |
|
Remove this pin from its group |
transition is "LINEAR" or "SMOOTH". torqueβs axis_component
is "PC1" / "PC2" / "PC3".
move_by over a Frame Range#
move_by ramps a translation of the pinned vertices by delta over the
frame_startβframe_end range, with a "LINEAR" or "SMOOTH"
transition. Pair it with unpin to release the pin once the move
completes.
pin = cloth.create_pin("Shirt", "SleevePins")
pin.move_by(delta=(0, 0, 0.5), frame_start=10, frame_end=30, transition="SMOOTH")
pin.unpin(frame=40) # release after the move finishes
Center-Mode Inference for spin and scale#
Pass whichever argument names your pivot, and the API picks the matching mode for you:
Argument you pass |
Inferred |
|---|---|
|
|
|
|
|
|
none of the above |
|
Passing center_mode="..." explicitly overrides the inference. See
Pins and Operations for what each
mode actually computes.
pin.spin(axis=(0, 0, 1), angular_velocity=360) # CENTROID
pin.spin(axis=(0, 0, 1), angular_velocity=360, center=(0, 0, 1)) # ABSOLUTE
pin.spin(axis=(0, 0, 1), angular_velocity=360, center_direction=(0, 0, -1)) # MAX_TOWARDS
pin.spin(axis=(0, 0, 1), angular_velocity=360, center_vertex=42) # VERTEX
Snap and Merge#
solver.snap("Shirt", "Mannequin") # translate Shirt onto nearest vertex on Mannequin
solver.add_merge_pair("Shirt", "Mannequin")
solver.remove_merge_pair("Shirt", "Mannequin")
solver.get_merge_pairs() # β [("Shirt", "Mannequin"), ...]
solver.clear_merge_pairs()
All of these share the same validation layer as the MCP interface and
the UI, so you get identical errors. Bad names raise ValueError.
Invisible Colliders#
Walls and spheres return a chainable builder. Parameters on .param
cover friction, contact_gap, thickness, and enable_active_duration
/ active_duration.
# A ground plane with extra friction.
solver.add_wall(position=(0, 0, 0), normal=(0, 0, 1)).param.friction = 0.5
# An inverted hemispherical container (keeps the cloth inside a bowl).
(solver.add_sphere(position=(0, 0, 0), radius=0.98)
.invert()
.hemisphere())
# A sphere that shrinks at frame 61.
(solver.add_sphere(position=(0, 0, 0), radius=1.0)
.time(60).hold()
.time(61).radius(0.5))
# A wall that slides to a new position.
(solver.add_wall(position=(0, 0, 0), normal=(0, 1, 0))
.time(60).hold()
.time(61).move_to((0, 1, 0)))
solver.get_invisible_colliders() # β [("WALL", "Wall"), ("SPHERE", "Sphere"), ...]
solver.clear_invisible_colliders()
Builder Surface#
Method |
Wall |
Sphere |
Purpose |
|---|---|---|---|
|
yes |
yes |
Advance the keyframe cursor (must be increasing) |
|
yes |
yes |
Hold the previous value at the cursor |
|
yes |
yes |
Keyframe a new position |
|
yes |
Keyframe a position offset from the previous |
|
|
yes |
Keyframe a new radius |
|
|
yes |
Keyframe position + radius together |
|
|
yes |
Flip inside-out (contact on the inside) |
|
|
yes |
Treat as a hemisphere |
|
|
yes |
yes |
|
|
yes |
yes |
Remove this collider |
See Invisible Colliders for how the keyframe timeline is evaluated.
Reset#
solver.clear()
Wipes every active group, resets scene parameters to their property
defaults, clears merge pairs, invisible colliders, dynamic parameters,
fetched frames, and residual MESH_CACHE modifiers. Run it at
the top of any script that needs a clean slate.
Fallback: Raw Operator Dispatch#
Anything not yet on the fluent API is reachable by attribute lookup.
Unknown attributes on solver fall through to
bpy.ops.zozo_contact_solver.<name>:
# Equivalent to bpy.ops.zozo_contact_solver.transfer_data()
solver.transfer_data()
# Keyword args are forwarded as the operator's properties.
solver.set(key="project_name", value="hero_shot")
Every MCP handler name (see MCP Server) has a matching operator, so whatever you can call over MCP you can also call here.
See Also#
Pins and Operations: the full semantics of
spin,scale,torque, and the center modes.Dynamic Parameters:
.dyn()keys,hold()/change(), and how they interpolate.Object Groups: what each group type actually does in the solver.
MCP Server: the same surface over JSON-RPC for agents.
Under the hood
The fluent API is a thin layer of proxy objects over the add-onβs operators and scene state:
solver.paramexposes a whitelisted attribute surface over scene-level properties. Reading an unknown name raisesAttributeError; assigning one goes through thezozo_contact_solver.setoperator, which reports an error and surfaces asRuntimeError.solver.param.dyn(name)returns a dynamic-parameter builder.solver.create_group(...)returns a group handle. Its.paramexposes that groupβs material/contact whitelist.group.create_pin(...)returns a pin handle; every mutating method returnsselfso calls chain.solver.add_wall(...)andsolver.add_sphere(...)return builder handles. The.time()cursor is tracked on the builder itself; frames must be strictly increasing.
The underlying proxy types are not part of the public contract. Pin
your scripts to the attribute and method names shown above, not to
isinstance checks.