Skip to content

Instantly share code, notes, and snippets.

@PardhavMaradani
Created April 7, 2025 04:33
Show Gist options
  • Select an option

  • Save PardhavMaradani/9c1c6ed229f83db7c3fc5ebf9abb3e8e to your computer and use it in GitHub Desktop.

Select an option

Save PardhavMaradani/9c1c6ed229f83db7c3fc5ebf9abb3e8e to your computer and use it in GitHub Desktop.
Learnings from explorations and prototyping

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.opengl is the call)

    • opengl context is not setup in the background (-b) mode, which is the same when using the bpy module 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 -P parameter) to generate the render output

      • Blender can be launched with the --no-window-focus param to not show up in the foreground and -p 0 0 0 0 to set the window parameters to minimum

      • To ensure the script runs after the file is loaded, bpy.app.handlers.load_post handler 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 .blend file - for example, annotations have to be present as object properties that can be read and drawn, etc

  • Rendering text/drawing annotations using the blf and gpu modules 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_view3d of gpu.types.GPUOffScreen to draw the viewport. This is an alternative to the bpy.ops.render.opengl method

  • 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 Convert node as well. To overlay this image correctly using Pillow (PIL), the image has to be converted from RGBa (true color with premultiplied alpha) to RGBA and 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 of Ctrl Alt 0). If the sensor size is 72mm, both the focal lengths (in the n panel > View and for the camera) should match (eg: 250mm). If the sensor size is 36mm, 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/gpu modules, 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.dpi and bpy.context.preferences.system.pixel_size have 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_ui can be set to True/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 using bpy.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_override can be used. Since there can be multiple windows with different areas and regions within, it is always safe to iterate through all for the required window/area/region before 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 of Render > Lock Interface in the UI) seems to help, which prevents viewport updates while rendering

  • bpy.context.scene.view_settings.view_transform = "Standard", which is the equivalent of Color Management > View Transform in 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

  • .vdb files (directly passed or converted from .dx files) 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/unpacking vdb files in Blender

  • 'Node To Python' extension generates code with numerical array indices - it is better to use the string names (Eg: Sphere As Mesh instead of 1) 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_x and so on unfortunately
  • 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_visibility in universe.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 to Preferences > Themes > 3D Viewport > Theme Space > Gradient Colors > Single Color). User preference changes are not part of the saved .blend file, instead they go to a separate userpref.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 to World. It is a bug/feature as described here
  • 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 a ReferenceError: StructRNA of type Object has been removed exception at times - one such case is when the object is selected and moved in the viewport using the g operator

  • 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_ui and update) but don't have update callbacks and cannot use bpy.msgbus subscriptions as well. Their application for UI needs is pretty limited

  • bpy.props cannot 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 a frame (bpy.props.IntProperty) with different min/max. The set/get have 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 like poll etc

  • In property update callbacks, self points to the property. For properties/property groups tied to objects, use self.id_data to access the object (or parent type) directly

  • Property update callbacks 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_item context. They lead to a AttributeError: 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_value in 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 to min/max valus 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_value for inputs. If the updated socket values have to be retained, they have to be copied over as the node input default_value if 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, .002 suffixes)

  • The only way to find nodes that belong to a particular frame is by checking the parent property of all nodes. Alternatively node names (see point above) can be saved in properties for direct access instead of full node traversal

  • The Socket_x value seen in the modifiers panel for group inputs can be obtained from the identifier attribute of the node_group.interface.items_tree[<input_name>]. The items_tree has 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 their update callback 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.blend file, which is different from the startup file startup.blend. Blender Application Template is one way to package both together

  • Disabling the Region Overlap option in Preferences > Interface > Editors of Blender is something we should recommend (or use an Application Template as 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 using bpy.data.objects.remove and passing the object

  • bpy.context.object and bpy.context.active_object are 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_object will be None. So, even though bpy.context.object is available for use in panels, the active_object property could be useful to distinguish between the active but hidden cases

  • bpy.msgbus can be used to subscripte to property changes of Blender datablocks. For example, to detect active object changes, bpy.msgbus.subscribe_rna has 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.props have an implicit name attribute. (It is ok to define it explicitly too) This applies to CollectionProperty as well and the find method of a collection property uses this name to 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 the index method of the list. Note that the name becomes 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 UILists can 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 Viewer extension 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 Item tab 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 PointerProperty can only hold references to Blender's own bpy.props or 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 update callback will NOT be called when keyframing that property. See Bug 86675 - need to use the set method as that is the only one that will get called

  • Properties cannot have just the set method alone - both set and get are needed. See Bug 107671

  • There is currently no way to mark a custom property as non-animatable. See BUg 113506

  • Muting the node (M key in GUI) or the mute boolean 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 Path constraint to work as intended. Though the Follow Curve option looks easier, it is always better to use the Fixed Position option and keyframe the Offset Factor value (between 0.0 - 1.0) for exact positioning on the path. Make sure the animation path is added using Animate Path button

  • Follow Path requires 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 the Follow Path starts

  • Use the Influence parameter 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 Properties matter. For example, if we want to change the focus to center2 from center1, the Influence of center2 should remain 1 and the Influence of center1 should keep decreasing from 1 to 0 - in addition, the Track To for center2 should be before that of center1

  • 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_set sets 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 a modal operator (timer event) 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 using bpy.ops.screen.animation_play() and in a frame_change_post callback doing the viewport render.

  • Blender app handlers (like frame_change_post, etc) are easy to add, but not straightforward to remove. Using the clear will 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 .blend file doesn't seem to include the universe topology filename (universe.filename) - it is None. The trajectory filename (universe.trajectory.filename) is present though - Fix in MN later

  • PIL does not have anti-aliasing for lines drawn using ImageDraw.line. The only alternatives are to have a custom implementation of anti-aliasing or draw to a bigger image and resize using Image.resize using the resample set to something like Resampling.LANCZOS. The later is what could be easier, but the font sizes and line widths have to be adjusted accordingly

  • PIL does 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 the datafiles/fonts directory

  • bpy.data.images["Render Result"] cannot be accessed to get the raw pixels of the render output. An alternative is to attach a Viewer Node in 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 as bpy.data.images in 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_add operator 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 using scene.sequence_editor_create and then adding images to an image strip works pretty well without any such limitations

  • Images appended to an image strip using elements.append will 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.actions and remove each action. obj.animation_data_clear() will only clear keyframes for the object but not ones associated with any geometry nodes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment