Blender
Common mistakes when driving Blender through MifBlender and MifBridge: names, deleting objects, color management, modifiers, physics bakes, undo, nodes, rigs and sessions. Read it when a Blender call answers ok and the scene is not what you asked for.
Blender renames instead of refusing, applies values quietly and caches things that look current. The pattern here is to read the answer's own fields (nameWasSuffixed, applied, warnings) and to measure the scene again after a change. For modeling craft see Blender modeling guide.
Names and datablocks
Deleting an object does not free its name
bl_delete_object removes the object, but the orphaned datablock keeps the name. Creating the object again answers ok with a .001 suffix and nameWasSuffixed: true. Nothing errors. Every later lookup by the original name then finds nothing, because that is what a name-keyed listing does when the name is absent.
Measured on Blender 4.4.0 from an empty scene: create, delete without purge, create again gives .001. Delete with purge: true reports orphansPurged: 72 and the next create gets the bare name.
- Tear a scene down with
purge: true. - When you build something, check the create response's
nameWasSuffixed. The call answersname,requestedNameandnameWasSuffixedprecisely because Blender renames instead of refusing. - This bites a long-lived Blender, not a fresh one. Running the same build twice in one session is where it shows up.
A refusal names the kind of datablock a parameter wants
When a lookup parameter names something that does not exist, the add-on answers in one fixed shape that states the kind:
add_cloth {"object": "__nope__"} -> no object named '__nope__'. Present: <scene is empty>
link_objects {"collection": "__nope__"} -> no collection named '__nope__'. Present: Collection.
The kinds the add-on looks up are object, node group, view layer, collection, material, action and world. The refusal also lists what is present, so read it before guessing again.
set_world with a name selects that world
A named world that exists is selected, and one that does not is refused in world_info's words. With no world at all, one is created under that name. Check the world field in the answer, since an older behavior edited whichever world the scene already had and answered ok:true.
clear_scene takes objectType, purge and purgeOrphans
A confirm key is refused. A setup call is a call whose answer must be checked: a refused clear_scene that is ignored leaves the default scene underneath everything you build.
Color management
The identifiers differ by Blender version, and a studio OCIO config renames everything. Read them from the build you have, because the enum a scene offers comes from the build's config.ocio. Measured on the stock configs:
| 3.6 | 4.2 | 5.0 and 5.2 | |
|---|---|---|---|
scene.display_settings.display_device |
sRGB, XYZ, None |
sRGB, Display P3, Rec.1886, Rec.2020 |
the 4.2 set plus Rec.2100-PQ, Rec.2100-HLG |
scene.view_settings.view_transform |
Standard, Filmic, Filmic Log, Raw, False Color |
those plus Khronos PBR Neutral, AgX |
20 entries, adding ACES 1.3, ACES 2.0, their HDR and SDR variants, the AgX HDR 1000 nits and SDR variants |
- The default view transform moved from
FilmictoAgX: 3.6.23Filmic; 4.2.17, 5.0.1 and 5.2.1AgX.Filmicis still present in 4.2 and later.XYZwas removed after 3.6. - A
lookcarries its prefix as part of the identifier.Medium High Contrastand the same name carrying theAgXprefix are different looks: the bare one applies under Filmic and the prefixed one under AgX. The bare names did not go away in 4.2. There is no barePunchyorGreyscale: they exist only with theAgXprefix. sRGBis a display device andStandardis a view transform. Neither is a legal value of the other's field.- In a
--backgroundBlender, reading the enum items of these properties returns['NONE']on every version while the property holds'sRGB'. A list that does not contain the value the property holds is not a list of what it allows.
Modifiers and meshes
A bevel over a boolean cut folds when it is wider than half the wall
A box with a round throat cut by boolean_op leaves each face as two concave n-gons around the hole. A bevel modifier on top renders clean at 0.5 mm and 2 mm and folds at 4 mm on a 4.2 mm wall, sweeping geometry across the throat.
| Bevel width on a 4.2 mm wall | Result |
|---|---|
| 0.5 mm, 2 mm | clean |
| 4 mm | folded, most of the opening covered |
| 4 mm after a Triangulate modifier | clean |
Modifier order is not the cause: boolean_op applies its cut to the base mesh, so bevel-then-cut and cut-then-bevel give the same 200-vertex mesh. Two bevels meeting inside a thin strip of a concave n-gon fold it. Triangulate before bevelling anything a boolean cut, and keep twice the width under the thinnest wall. Solid shading hides the fold. bl_add_modifier reports it with triangleOverlapBefore, triangleOverlapAfter (triangle area over face area, 1.000 when nothing folds) and facesFold, and warns to use Triangulate for a bevel.
A snapshot field answers the question it was taken for
None of these fields is wrong, but each answers the question next to yours:
| Field | What it measures | What a caller took it for |
|---|---|---|
evaluatedUnchanged on adding a modifier |
whether the modifier changed the evaluated mesh at the moment it was added | whether it will change the mesh. A Lattice modifier added before its lattice points move is the identity at that moment. |
decimate_mesh dry run |
trisBefore, ratioRequested, targetTris |
a prediction of trisAfter. Only the collapse mode's is ratio times trisBefore. |
dimensionsBU |
Blender's obj.dimensions, a cache refreshed on the next depsgraph update |
the size now. For an object with a viewport modifier or shape keys it is measured off the evaluated vertices, with Blender's own number as blenderDimensionsBU. |
transform_selection changed on a dry run |
whether the predicted coordinates differ | whether the mesh changed. A dry run answers false, with wouldChange carrying the prediction. |
Ask for the state again after the thing that changes it.
Shape keys deform without a modifier
A mesh with shape keys beyond its basis evaluates to the keyed shape with an empty modifier stack, so a "no modifiers means the base mesh" shortcut reads the rest pose. Blender also applies no modifier into a keyed mesh: bpy.ops.object.modifier_apply refused Displace, Simple Deform, Smooth and Solidify alike with "Modifier cannot be applied to a mesh with shape keys" on 3.6.23, 4.2.17, 4.4.0, 5.0.1 and 5.2.1. The ways through are asShapeKey: true on apply_modifier (a deforming result captured as a new key) or apply_shape_keys first.
An FBX export with modifiers applied (the default) writes no shape keys at all, and an Armature modifier counts too when its rig is not in the file. export_mesh names the meshes in shapeKeysDropped.
Other modifier traps
- Removing a modifier that came from a library override returns, the stack reads it gone, and after a save and reload it is back. Only the operator's poll refuses it.
remove_modifierandremove_constraintrefuse such an item. A modifier added locally on the override is removed for good. - From 4.4,
modifiers.newon a Volume object returns a modifier for every grease pencil type and Lineart, which do nothing there. A Curves or point cloud object takes Nodes modifiers. - An Ocean modifier with foam on writes foam into the evaluated mesh as a color attribute that is named
Colunless the modifier's foam layer name (modifier.foam_layer_name) is set. A material's Attribute node reads a layer by name, so the usual recipe reads nothing and the foam never shows. Set the name first (set_modifierwith the foam layer name insettings), then read it back frommesh_statsevaluated.attributesAddedByModifiers. A JONSWAP spectrum makes almost no foam where PHILLIPS foams freely (PHILLIPS spans 0.0 to 1.0, mean 0.28; JONSWAP sits between 0.093 and 0.105), so a foam ramp tuned under one spectrum renders nothing under the other. clean_mesh's merge distance is in mesh units. A child of a parent scaled 100 welds 100 times the world distance asked.convert_objectbakes the rig's current pose, so a skinned mesh converted while its action sits at frame 30 keeps the frame-30 pose. Both now reportspaceandpose.
Visibility
The outliner's eye is the view layer's hide state, Object.hide_get() and hide_set(), not a datablock flag. Object.hide_viewport (the monitor icon) can read False while the object is hidden there, and an operator that works on the selection, such as a selected-to-active bake or a convert, then refuses with "No valid selected objects". visible_get() is Blender's own is-it-drawn and folds in every hide flag and the collections above. object_info carries hiddenInViewLayer and visibleInViewLayer, and set_object_visibility writes hiddenInViewLayer.
Physics
A rigid body bake outlives the bodies it was baked for
A rigid body cache is stored by position in the world's collection, not by object. A stale bake is therefore not just old: it hands frames to the wrong bodies. Measured identically on Blender 3.6.23, 4.2.17, 4.4.0, 5.0.1 and 5.2.1, for a baked world with a floor and bodies A and B:
| What happened | Result |
|---|---|
| delete A | B at frame 10 stood at A's own frame-10 position; the cache still read baked |
clear_scene, build a new floor and body D, bake |
D replayed A's frames, with keptExistingBake: ["rigidbody"] |
| add a body C | C did not move on any frame; a plain bake kept the old frames |
| add C, bake, then rebake (free all, bake) | C still did not move |
Variants like collision shape, friction and bounciness looked inert in one scene because every build collapsed to the first fixture's numbers. In a fresh scene all of them work. Deleting a body out of a baked world frees that world's bake (freedRigidBodyBake), add_rigid_body into a baked world warns with bakedWorldNote, and bake_physics with rebake: true rewrites the world's substeps per frame onto itself before a bake from nothing. Bounciness is a property of both surfaces: a sphere at 0.99 rebounds 0.00 m off a floor at 0 and 2.29 m off a floor at 0.99 (bouncinessNote).
Other bake traps
- A cloth stepped frame by frame fills its cache without a bake, and freeing the bake leaves it. A disk cache of 20 stepped frames is 20
.bphysfiles.bake_physicswithclearleaves the scene at the start frame and says so. point_cache.use_disk_cache = Trueon an unsaved file is declined without an error and reads back False. Blender keeps a disk cache only beside a saved .blend. A plainsave_filesaves a copy and leaves the session unsaved. Usesave_filewithrepointSession: true.- A node simulation with a DISK bake target and an empty bake directory answers FINISHED and plays back, and no file appears.
bake_physicssets//<object>_<modifier>_bake/when a disk bake has none. A node bake also steps the scene through its whole range, so a cloth in the scene fills its memory cache along the way. - Blender keeps no baked flag for a node simulation. The measurable sign is playback: jumped to frames 5, 1, 3 a baked zone adding 1 in Z a frame reads 6, 2, 4, and live it reads 3, 2, 3.
- Cycles' AO bake takes its occluder distance from the scene's world,
light_settings.distance(10 on a factory startup), not from the bake operator. Two planes 1 m apart baked white at 0.1 and to a mean of 0.69 at 10.bake_textureaoDistancesets it for one bake and restores it.
Undo
A bpy.ops call from Python pushes no undo step
Measured on 3.6.23, 4.2.17, 4.4.0, 5.0.1 and 5.2.1, plain bpy in background mode:
| Sequence | Undo |
|---|---|
base push, primitive_cube_add() |
fails with "poll() failed", nothing to undo, the cube stays |
base push, primitive_cube_add('EXEC_DEFAULT', True) |
the cube goes, only this form pushes |
| add an object (pushed), add another (not pushed) | both go in one undo, and redo brings back only the pushed one |
| push twice with nothing changed | the second push is a step: an undo spends itself on nothing |
Push after every call that can change the scene, including refusals, and never after a read. MifBlender does this for the bridge. Code that drives Blender directly has to do the same or have nothing to undo.
The operator log has the same dependence on that undo argument. WindowManager.operators (what Adjust Last Operation and Repeat read) takes an operator that finishes at undo depth 0. bpy.ops.X(...) does not, because the undo argument defaults to false, and bpy.ops.X('EXEC_DEFAULT', True, ...) is logged. "The log is empty headless" is a measurement of undo false. call_operator's undo key is the route, and list_recent_operators reads the result.
Nodes
A new link into an input that already has one replaces it
node_tree.links.new(out, in) into a single input that is already linked removes the old link first. It raises nothing and returns the new link, and the tree's link count does not change, so a caller comparing counts sees no change where a wire was swapped. A multi-input socket (Join Geometry, socket.is_multi_input) keeps both.
- Match a link by socket identifier, not name. A Math node's three inputs are all called
Value; their identifiers areValue,Value_001andValue_002. - A linked input's
socket.default_valueis still writable and still reads back, while Blender renders the link and never the value. Writing it is a silent no-op that confirms itself. link_group_nodesreturns the displaced link asreplacedLinks, enough to restore it, and takes links out withremove: true.set_node_propertyrefuses to write a linked input.- Setting
node.parentto a Frame node turnsnode.locationinto an offset from the frame. A Cube node made at (120, 60) and parented to a frame at (100, 50) reads (20, 10). - A light's
light.use_nodesswitch is ignored from Blender 5.1: a light always renders its tree, and switching it off never removed an IES profile.set_light_ieswith clear now removes the IES node, so a cleared light renders as a plain one.
Rigs and animation
- A pole angle with the wrong sign bends the knee backwards. The angle that lays an IK chain on its rest pose is the signed angle from the base bone's X axis to the pole plane, negative when the cross product points along the bone. With the sign the other way, the leg's IK knee sat 0.1 behind the deform knee at rest. The arm bends the other way and hid it.
add_control_rigreportsrestErrorper chain, which catches it. - A bone is a new wrapper on every access.
b.parent is pis never true for two bpy bones, even the same one. Compare names. - A bake's own check is blind under live constraints. A deform copy driven by Copy Transforms is still driven at both ends of a measurement, so a before-and-after check reads 0 whatever the keys hold. Replay each baked action with every constraint muted against the rig instead.
- FBX all-actions off writes nothing while NLA is on. The exporter writes the scene-timeline take only when both the "NLA strips" and "all actions" options are off. Both default to true, and with all-actions off and NLA left on, a rig with a keyed active action exported no curves and no error.
- Scaling connected bones one at a time moves joints twice. A connected child's head is its parent's tail, so scaling an armature's bones one by one (EditBone.transform included) moved every shared joint twice: a human metarig scaled 3x came out 17.3 m tall instead of 5.94. Compute every head and tail first, then write them. Edit mode also keeps a connected child's head on its parent's tail.
- Rigify enabled without its preferences entry is half there.
addon_utils.enable("rigify", default_set=False)half-registers the generator, and generation throws "'RigifyParameters' object has no attribute 'make_custom_pivot'" with a 159-bone partial rig. Enable it withdefault_set=Trueand putpreferences.is_dirtyback. - A Rigify generator failure arrives after the rig.
pose.rigify_generatereports a failure withreport(ERROR)and returns FINISHED, sobpy.opsraises after the target rig is half-built, and on a regenerate after its old bones and collections were removed. Copy the old rig first. - The pose library's operators need a UI context. Its apply operator cannot run headless on any build ("poll() failed, context is incorrect" on 3.6.23 and 4.4.0, "No asset in context" on 5.2.1).
create_pose_assetandapply_pose_assetdo the work themselves. - Bone selection moved from
Bone.selecttoPoseBone.selectin 5.x. - Quick Fur under a context override puts the hair modifiers on the mesh.
object.quick_furreadscontext.active_objectfor the new hair. Called undertemp_override(active_object=surface)that read returns the surface (measured on 5.2.1). Select the surface and make it the view layer's active object instead. Quick Fur also skips a mesh with no UV map with FINISHED and a warning, and still creates a "Fur Material", an orphan.
UVs
bpy.ops.uv.pack_islandsin a session with no UV editor packs as if the texture were square. An island stretched in V for a 1024 x 512 texture came out of a rotating pack (rotate methodANYorAXIS_ALIGNED) with texels 2.65 times longer one way than the other, on 3.6.23 and 5.2.1.uv_packrefuses rotation on a non-squaretextureSizefor this reason.- A concave pack with rotation is slow on 3.6.23: 15 to 23 s for a six-island cube, against under 0.01 s packing bounding boxes (shape method
AABB) and under 1 s on 5.2.1. - A UV layer reference is invalid after an edit-mode toggle. Writing
uv_layers.active.data[i].uvthrough a reference taken beforebpy.ops.object.mode_setcrashed 3.6 and 5.2. Fetch the layer again after every toggle.
The session
- A factory reset inside
run_pythonstops MifBlender.bpy.ops.wm.read_factory_settingsandbpy.ops.wm.read_factory_userprefdisable every add-on. MifBlender stops its server, and a--backgroundBlender with nothing left to run quits.run_pythonrefuses both, plus a Blender quit call and disabling or removing MifBlender. For an empty scene usebl_clear_sceneorread_homefile(use_empty=True), which leaves the server answering. - A headless Blender serves one connection at a time. In background mode the accept loop handles each client inline, so a second connection waits until the first one closes. A second client talking to the same headless Blender waits out its whole timeout. Close the first connection before the next client's call, and start a separate Blender for a separate process. A GUI Blender gives each client a thread. See MifBlender MCP.
- A background Blender never retires a job. The job timer that retires a window-manager job belongs to the UI, which a
--backgroundBlender lacks. On 5.2sound.mixdownanswers FINISHED before the file exists and later mixdown jobs in that process never write. After a proxy build, clip tracking operators answer CANCELLED for the rest of the session and leave the markers where they were, so track before building proxies. See Common mistakes with audio. - Clearing a scene's sequence editor and then changing its frame rate crashes Blender.
Scene.sequence_editor_clear()followed by any change ofrender.fpskills Blender with an access violation (4.4.0, 5.0.1 and 5.2.1, background). A depsgraph update between the two does not help, and making the editor again does. Remove the strips instead of clearing the editor.