This document lists learnings from explorations and prototyping of MDAnalysis GSoC 2025 - Project 4: Better interfacing of Blender and MDAnalysis
-
Viewport rendering uses opengl (
bpy.ops.render.openglis the call)-
opengl context is not setup in the background (
-b) mode, which is the same when using thebpymodule in headless mode -
The only way to get a viewport rendering (or anything opengl related) in background mode is to save the file and launch it with Blender in the regular GUI mode and run a script (with the
-Pparameter) to generate the render output-
Blender can be launched with the
--no-window-focusparam to not show up in the foreground and-p 0 0 0 0to set the window parameters to minimum -
To ensure the script runs after the file is loaded,
bpy.app.handlers.load_posthandler has to be used -
The passed script has to quit Blender once done using
bpy.ops.wm.quit_blender() -
All data required for rendering has to be part of the
.blendfile - for example, annotations have to be present as object properties that can be read and drawn, etc
-
-
-
Rendering text/drawing annotations using the
blfandgpumodules will require to an offscreen buffer using opengl - hence this will not work in the background mode for above reasons. Same workaround as above works here too. Using PIL to render to images as described later is a workable solution -
Both the viewport and annotations can be drawn to an offscreen buffer in a single pass to get the viewport render. This requires using
draw_view3dofgpu.types.GPUOffScreento draw the viewport. This is an alternative to thebpy.ops.render.openglmethod -
If the annotations render image has to be composited with another render (which will be the case with regular renders), note that the annotation render image (the one extracted from the offscreen buffer) has premultiplied alpha. This is because all of Blender's internal rendering uses premultiplied alpha. Premultiplied alpha is lossy and you cannot recover the original rgba correctly. This is the same with Blender's
Alpha Convertnode as well. To overlay this image correctly using Pillow (PIL), the image has to be converted fromRGBa(true color with premultiplied alpha) toRGBAand then pasted. Any other method will result in an incorrect alpha output -
For the camera to match the viewport view, it is not enough to just call
bpy.ops.view3d.camera_to_view()(the equivalent ofCtrl Alt 0). If the sensor size is72mm, both the focal lengths (in then panel > Viewand for the camera) should match (eg:250mm). If the sensor size is36mm, the focal length of the camera must be half that of the viewport (eg:125mm). Without the above any overlay content (like a separate annotations render) will not match the camera renders and what you see in the viewport is not what you get in viewport renders -
For text and lines drawn using
blf/gpumodules, there is a font size and line width discrepancy that depends on the screen resolution. Apparently Blender uses a dpi of 72 internally.bpy.context.preferences.system.dpiandbpy.context.preferences.system.pixel_sizehave the dpi and pixel size data. (MacOS retina has 144/2 and regular ones 72/1) Text/line widths have to be scaled accordingly -
Controlling the navbar panel (or n-panel) in the 3d viewport is a bit finickly.
bpy.context.space_data.show_region_uican be set toTrue/False. Alternatively,bpy.ops.wm.context_toggle(data_path='space_data.show_region_ui'), does the same. But the updated value doesn't reflect (even after a force redraw of viewport or view layer update usingbpy.context.view_layer.update()) (only an issue when running all cells at once) -
Most internal operators require the context to be set correctly before they are invoked.
bpy.context.temp_overridecan be used. Since there can be multiple windows with different areas and regions within, it is always safe to iterate through all for the requiredwindow/area/regionbefore setting the context so that everything is updated correctly -
Most Blender crashes are due to accessing something before the viewport got updated. Ensuring that the viewport is redrawn eliminates most of these crashes.
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)as outlined in the Blender Gotchas page is one quick way, but is not guaranteed compatibility in future. Timers/Handlers/Modals are other recommended ways -
Setting
bpy.context.scene.render.use_lock_interface(equivalent ofRender > Lock Interfacein the UI) seems to help, which prevents viewport updates while rendering -
bpy.context.scene.view_settings.view_transform = "Standard", which is the equivalent ofColor Management > View Transformin Render/Output properties seems to generate sharper viewport render images -
bpy.ops.view3d.zoom_border(...), the equivalent of of box zoom in viewport (Shift B) doesn't work in background mode - don't use it -
The simplest way to frame any region of interest is to create a temporary object with only the bounding box vertices and frame that object using
bpy.ops.view3d.view_selected() -
Force redrawing viewport using
bpy.ops.wm.redraw_timer(type='DRAW_WIN_SWAP', iterations=1)doesn't work in background mode - don't use it in that mode -
MN doesn't support comma separated selection strings in attribute values - this can be very useful to get ordered selections as supported in this API. This can potentially be added in MN later
-
.vdbfiles (directly passed or converted from.dxfiles) have to persist if the file has to be opened again (for example headless mode viewport rendering case). There also seems to be a known issue with packing/unpackingvdbfiles in Blender -
'Node To Python' extension generates code with numerical array indices - it is better to use the string names (Eg:
Sphere As Meshinstead of1) so the code is clear what is being updated/linked/unlinked. The names can be seen from the 'Python Console' as auto-fill options or checked under the Node details- This doesn't apply to modifier inputs used in custom panels - they still have to be referenced as
Socket_xand so on unfortunately
- This doesn't apply to modifier inputs used in custom panels - they still have to be referenced as
-
Suppressing outputs has to be done at the correct place - cannot be global.
with redirect_stdout(io.StringIO()):is a quick way for stdout and there is an equivalent for stderr. -
There is a toggle visibility bug in Blender where icons don't reflect the correct value and another issue with volume objects (see
_toggle_visibilityinuniverse.py) - harmless -
One way to change the background color of the viewport is to change in the theme (
bpy.context.preferences.themes["Default"].view_3d.space.gradients.high_gradient = (r, g, b), which is equivalent toPreferences > Themes > 3D Viewport > Theme Space > Gradient Colors > Single Color). User preference changes are not part of the saved.blendfile, instead they go to a separateuserpref.blend. This can be an issue in the background mode.- A better way is to use the
World > Viewport Display > Color, but this requires the solid mode viewport shading background set toWorld. It is a bug/feature as described here
- A better way is to use the
-
Always use Blender's portable installation to keep environments separate and clean
-
An object's
visible_get()method to get whether the object is visible or not can throw aReferenceError: StructRNA of type Object has been removedexception at times - one such case is when the object is selected and moved in the viewport using thegoperator -
Blender ID properties (non API defined custom properties) are very different from
bpy.props(API defined). A good guide to some of the basic differences are highlighted here -
ID properties having nested levels are not possible to access through the UI (panels, etc). They have to be flat and at the top level of an instance (like object). They can be used as drivers, can be completely customized for ui (using
id_properties_uiandupdate) but don't have update callbacks and cannot usebpy.msgbussubscriptions as well. Their application for UI needs is pretty limited -
bpy.propscannot be setup for instances and can only be setup for types and hence apply to all instances. Once defined, the min/max and other values cannot be updated without re-defining it (which changes for all instances). For example, different objects cannot have aframe(bpy.props.IntProperty) with different min/max. Theset/gethave to be used to enforce limits. These are however the best option for UI and other use inspite of some limitations -
Panels can use a mix-in parent class as specified here to not repeat the common
bl_values and methods likepolletc -
In property update callbacks,
selfpoints to the property. For properties/property groups tied to objects, useself.id_datato access the object (or parent type) directly -
Property
updatecallbacks have to be carefully used. There are several caveats as noted in the bpy.props page. There are also no safety checks for infinite recursion and hence the need for them and the ordering should be carefully considered -
Property changes/updates cannot happen in the
draw/draw_itemcontext. They lead to aAttributeError: Writing to ID classes in this context is not allowed:error. So, any property changes that need to reflect when drawing the layout should be updated in a different (like operator/API) context -
The
default_valuein the inputs/outputs of Geometry Nodes is the value that the input/output gets when instantiated. Just because it is connected to a socket does not mean that the socket gets this value automatically. This also applies tomin/maxvalus for ints/floats. For example, if there are new sockets that are connected to certain inputs, the socket values will not take the default values till a new instance is created. In case of modifiers this happens when the modifier is re-created. The alternative is to copy over the default values to the sockets when they are linked -
Similar to above, changes to the socket values (either from the modifiers panel or any custom panel) don't update the node's
default_valuefor inputs. If the updated socket values have to be retained, they have to be copied over as the node inputdefault_valueif they have to be used again -
Always use the node name that Blender assigns (after taking into account duplicates) than the node name that is coded (eg: names could end up with
.001,.002suffixes) -
The only way to find nodes that belong to a particular frame is by checking the
parentproperty of all nodes. Alternatively node names (see point above) can be saved in properties for direct access instead of full node traversal -
The
Socket_xvalue seen in the modifiers panel for group inputs can be obtained from theidentifierattribute of thenode_group.interface.items_tree[<input_name>]. Theitems_treehas all the input details along with the type and such -
Modifier inputs accessed from other custom panels will draw the
PointerPropertieslike Material etc as disabled (grayed out) values and hence they cannot be updated. (All other properties work fine) The workaround is to use a custom property for such items and in theirupdatecallback set the modifier inputs. If geometry node inputs are not required in the modifier panel, custom panels can directly use the data path to link to specific node inputs -
Blender user preferences are stored in a separate
userpref.blendfile, which is different from the startup filestartup.blend. BlenderApplication Templateis one way to package both together -
Disabling the
Region Overlapoption inPreferences > Interface > Editorsof Blender is something we should recommend (or use anApplication Templateas mentioned above). With this, the sidebar will no longer be an overlay, but a region by itself. This allows the 3d viewport to not get hidden behind the sidebar and is especially useful when trying to frame selections and other GUI operations where the sidebar could be a ever present -
The most common way to delete an object is to select it and then call an operator like
bpy.ops.object.delete(). This can be problematic at times if the view layer is not up to date and whatever is the active object will get deleted. A better way is to explicitly delete the object usingbpy.data.objects.removeand passing the object -
bpy.context.objectandbpy.context.active_objectare not necessarily interchangeable. The Context Access page specifies the contexts in which the properties are available. One key point to note is that when an object is active, but hidden,bpy.context.active_objectwill beNone. So, even thoughbpy.context.objectis available for use in panels, theactive_objectproperty could be useful to distinguish between the active but hidden cases -
bpy.msgbuscan be used to subscripte to property changes of Blender datablocks. For example, to detect active object changes,bpy.msgbus.subscribe_rnahas to be used for the key(bpy.types.LayerObjects, "active")- this is the only way. This works for all RNA properties (builtin ones and custom ones) and won't work for ID properties as noted before -
bpy.propshave an implicitnameattribute. (It is ok to define it explicitly too) This applies toCollectionPropertyas well and thefindmethod of a collection property uses thisnameto return the index of an item if present. This can be very helpful. Without this, the alternative is to convert the keys to a list and use theindexmethod of the list. Note that thenamebecomes part of a tuple during insertion and hence immutable - use a constant value like a hash before insertion (and not something like object name that can change) -
The list index property when using
UIListscan be set to -1. This is the equivalent of nothing selected in the displayed UI list. Always check that the list index is in range before displaying any item details (to avoid out of bound errors) - Blender only sets the value when a UI list item is clicked but updating it when items are added/deleted is user's responsibility -
Use Blender's builtin
Icon Viewerextension to visually see and select icons for use in GUI -
All other built-in tabs (Tool/View) can be removed from the sidebar, but the
Itemtab is drawn from Blender's C code and hence cannot be removed/hidden -
Always use Blender properties to keep track of state - this allows interoperability between API and GUI modes. Blender's
PointerPropertycan only hold references to Blender's ownbpy.propsor ID properties and not arbitrary data. There is no way to reference your own python class/object from a Blender property. This is a very important point to consider during implementation of any API -
Properties that have an
updatecallback will NOT be called when keyframing that property. See Bug 86675 - need to use thesetmethod as that is the only one that will get called -
Properties cannot have just the
setmethod alone - bothsetandgetare needed. See Bug 107671 -
There is currently no way to mark a custom property as non-animatable. See BUg 113506
-
Muting the node (
Mkey in GUI) or themuteboolean attribute (in scripts) is a quick way to disable the node without deleting it or the links -
Changing the node tree in a property callback (like
set) could lead to a Blender crash when the property is keyframed (see gotchas). The workaround is to do the updates in a one off timer (bpy.app.timers.register(_update_func, first_interval=0.001)). An alternative is also described in the Application Timers page. It is best to avoid any node tree changes during the rendering pass -
Camera transforms have to be reset for the
Follow Pathconstraint to work as intended. Though theFollow Curveoption looks easier, it is always better to use theFixed Positionoption and keyframe theOffset Factorvalue (between 0.0 - 1.0) for exact positioning on the path. Make sure the animation path is added usingAnimate Pathbutton -
Follow Pathrequires camera transforms to be reset as mentioned above. For this reason, this doesn't work well with arbitrary camera placement and keyframing it - unless the transforms are reset (via a keyframe) before theFollow Pathstarts -
Use the
Influenceparameter in the constraints (Follow Path,Track To, etc) to control the amount of influence each has on the end result. Having multiple influences at the same time could lead to unwanted results. Keyframing this value gives better control. Tracking from one point to another can also be achieved using this -
The order in which constraints appear in the
Object Constraint Propertiesmatter. For example, if we want to change the focus tocenter2fromcenter1, theInfluenceofcenter2should remain 1 and theInfluenceofcenter1should keep decreasing from 1 to 0 - in addition, theTrack Toforcenter2should be before that ofcenter1 -
For smooth movement along a curve, the curve type should be bezier - especially if the curve was converted to from a mesh. The control point handle type should be set to auto and the curve should be set to smooth (and segments subdivided if necessary)
-
Using
bpy.context.scene.frame_setsets the frame to a given value. Trying to do a viewport render right after it might lead to outputs with viewport not updated. Forcing a re-draw of the viewport or doing this in amodaloperator (timerevent) will help for a single output image, but doing this in a loop will still lead to viewport outputs not updated. For rendering viewport animation frames, the only workaround that worked is the following: Simulating the play animation usingbpy.ops.screen.animation_play()and in aframe_change_postcallback doing the viewport render. -
Blender app handlers (like
frame_change_post, etc) are easy to add, but not straightforward to remove. Using theclearwill remove all handlers, which is not desirable. The alternative is to remove by using the actual handler name (the function name) -
A saved and re-loaded
.blendfile doesn't seem to include the universe topology filename (universe.filename) - it isNone. The trajectory filename (universe.trajectory.filename) is present though - Fix in MN later -
PILdoes not have anti-aliasing for lines drawn usingImageDraw.line. The only alternatives are to have a custom implementation of anti-aliasing or draw to a bigger image and resize usingImage.resizeusing theresampleset to something likeResampling.LANCZOS. The later is what could be easier, but the font sizes and line widths have to be adjusted accordingly -
PILdoes not have text rotation support. The recommendation for this support is to draw the text on a separate image, rotate the image and paste it - not ideal -
Blender's current default font is
Inter(4.x onwards) - the font files are in thedatafiles/fontsdirectory -
bpy.data.images["Render Result"]cannot be accessed to get the raw pixels of the render output. An alternative is to attach aViewer Nodein the compositor and access the pixels. However, the pixel values aren't entirely between 0 and 1 and hence cannot be converted to a PIL image, etc. As outlined here the viewer node output isn't color corrected etc. So there is no way of accessing the raw bytes (that match the file output) before it is written out to a file -
PIL images have to be flipped (
numpy.flipud) for use asbpy.data.imagesin compositor -
If geometry node values need to be exposed in the modifier UI, they have to be linked to group inputs. Any geometry node input can be linked directly in a UI panel as long as the data path is set correctly.
-
Though
bpy.ops.sequencer.image_strip_addoperator adds images to sequence editor, it requires the correct context. To setup the context, one of the regions must be set to the video sequencer, which creates problems for the GUI mode. Instead, creating a sequence editor usingscene.sequence_editor_createand then adding images to an image strip works pretty well without any such limitations -
Images appended to an image strip using
elements.appendwill only have to pass the base name of the images and not the full path -
Video sequence editor has to be created (and deleted after generating the video) after rendering all the frames, else it will interfere with the rendering as it has higher precedence
-
The easiest way to remove all the keyframes is to iterate through
bpy.data.actionsand remove each action.obj.animation_data_clear()will only clear keyframes for the object but not ones associated with any geometry nodes