Blender
Props and GUI
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 responsibilityBlender’s
UIListhas a way to both filter (using bit fields) and re-order items using thefilter_itemsmethod. There are also some helper methods that Blender provides inbpy.types.UI_UL_listthat helps both. The management of active index of the selected item in the UIList becomes tricky in case of filteringProperty 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) contextPanels can use a mix-in parent class as specified here to not repeat the common
bl_values and methods likepolletcBlender allows panels in the UI layout drawn by users (using
layout.panel(...)). This allows better organization in what we displayPanels created using
<layout_item>.panel()in Blender can beNoneimmediately after, so a guarding check is needed to use this correctly in the UI codeA
CollectionPropertywith a dynamicPropertyGroupcan be assigned to standard types (likebpy.types.Object) at runtime. This is the key that allows extensibility. Blender converts these to ID properties when the corresponding type classes are not registered. Once the type classes are registered they become API defined properties- The property path has to be at the top level and not nested under another property. For example,
bpy.types.Object.mn_annotationsworks, butbpy.types.Object.mn.annotationsdoesn’t. The later leads to aAttributeError: '_PropertyDeferred' object has no attribute '...'error
- The property path has to be at the top level and not nested under another property. For example,
.bl_rna.propertiesof aPropertyGroupcan be used to iterate all the properties within aPropertyGroup. The property name is available under theidentifierattribute of the iterated item. Thetypeattribute specifies the property type (likePOINTERetc)Changing Blender properties through API will need the viewport to be tagged for redraw (
area.tag_redraw()) to show updated values immediately in Blender’s GUIbpy.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 limitsIn property update callbacks,
selfpoints to the property. For properties/property groups tied to objects, useself.id_datato access the object (or parent type) directlyProperty
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 consideredA lambda function can be used to pass the actual property name within a property group that caused the update in the
updatecallback. Care should be taken when using lambda functions in a loop - a closure is needed to ensure that correct values are passed - this can be done using defaults, a custom function etcAlways 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 propertyProperties 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 calledProperties cannot have just the
setmethod alone - bothsetandgetare needed. See Bug 107671There is currently no way to mark a custom property as non-animatable. See BUg 113506
Using a custom property in the
getterandsetterof a blender properties interface allows the use of both blender property types and any custom types for API use. We use this trick to supportAtomGroupselections through the API. Blender property update callbacks being different from the API basedgetter/setterallows for this to work seamlesslyThere is no way to specify a filter for specific files when using Blender’s
StringPropertywith subtypeFILE_PATHdirectly - users can filter from Blender’s file selection GUIBlender’s
EnumPropertydefaults to the first value when the default is not specified, so it is good to explicitly set a defaultBlender 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 (usingid_properties_uiandupdate) but don’t have update callbacks and cannot usebpy.msgbussubscriptions as well. Their application for UI needs is pretty limited
Operators
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 correctlywindow_manager.invoke_confirmin an operator’sinvokeis used for confirmation dialogs.window_manager.invoke_props_dialogin an operator’sinvokeis used to show a dialog with the operator properties. An explicitdrawmethod can be used to customize the layout of what is shownThe
executemethod of operators can be directly invoked (bypassinginvokeetc) using the first param asEXEC_DEFAULT- this is very useful in tests to test operators.EXEC_INVOKEcan be used to directly call the operator and run it’s invoke methodThe 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. Avoid using operators if the same can be achieved in a different way. Operators are both slow and can be asynchronous with different context requirementsBlender does not allow dynamic properties in an operator. To achieve dynamic properties in an operator (to show in the
invokeUI for example), a temporary operator has to be created, registered with Blender and invoked. A dynamicPropertyGroupwith dynamic properties can be created using thetype()method. This dynamic type can be used within aPointerPropertyof the temporary operator for dynamic inputsbpy.types.Operator.__subclasses__()andbpy.types.PropertyGroup.__subclasses__()can be used to verify whether temporary operators and property groups are being unregistered and garbage collected.sys.getrefcountcan be used on a class to get the reference counts
Viewport
An object’s
hide_viewportproperty is not animatable. To support visibility changes that are animatable, a custom property (withgetandsetcallbacks) that controls both the viewport and rendering visibility works betterAn 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 thegoperatorbpy.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 casesThe
view_matrixof Blender’s region data can be used to determine the distance of a 3D point to the virtual 3D viewport camera. This works for both theothographicand the more commonperspectiveprojection modes. A range of the distance values for an object can be determined by calculating the min and max of the object’s bounding box vertices (8 of them). This allows for a really fast way to determine on a normalized scale how far a point in the object space is from the 3D viewport. We use this to adjust the text size of annotations to create a perception of depth.Blender uses a bit of magic math to display the camera view outline in the 3D viewport. The actual camera width and height shown and the position can be determined based on the
view_camera_zoomandview_camera_offsetparams of the region’s 3d data. There is also a dependency on the viewport aspect ratioBlender’s
camera_to_view_selected(bpy.ops.view3d.camera_to_view_selected()) is a great way to frame a selection. Care should be taken to ensure only what is required is selected (and restored) before calling this operatorViewport rendering uses OpenGL (
bpy.ops.render.opengl). OpenGL context is not setup in the background (-b) mode, which is the same when using thebpymodule in headless modeMost 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 waysForce 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
Rendering and PIL
Blender’s current default font is
Inter(4.x onwards) - the font files are in thedatafiles/fontsdirectory. PIL needs access to the font file to display textPIL 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 usingImage.resizeusing theresampleset to something likeResampling.LANCZOS. For the later, font sizes and line widths have to be adjusted accordinglyPIL’s
textbboxcan be used to get the bounding box - to determine the text width / heightPIL’s co-ordinate system is from the top left, where as Blender’s 3D viewport is from bottom left
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
object_utils.world_to_camera_view(from bpy_extras) can directly be used to get the corresponding 2D coords of a 3D point in the camera space for renderingIn the render mode, we can use the inverted value of the world matrix (
camera.matrix_world.inverted()) as the view matrix for any computationsMost of what is available through operators for Blender’s Video Sequence Editor (like
bpy.ops.sequencer.image_strip_add) can be done through API as well (scene.sequence_editor_create), which has the added advantage that it is faster and doesn’t require the context to be presentImages appended to an image strip using
elements.appendwill only have to pass the base name of the images and not the full pathVideo 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, so they must be cleared if setup temporarily
Setting
bpy.context.scene.render.use_lock_interface(equivalent ofRender > Lock Interfacein the UI) seems to help, which prevents viewport updates while renderingbpy.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. The default from Blender 4.0 isAgX. If annotations with white(1, 1, 1, 1)don’t show up as such in renders, this is the reasonbpy.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 filePIL images have to be flipped (
numpy.flipud) for use asbpy.data.imagesin compositor
Nodes
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 not have the node tree structurally change during the rendering processNode Groupinstances can be decoupled from one another by simply making a copy of the node tree - this is also equivalent to making a data block as single-user in Blender. This allows changes to each of the node groups to be independent of othersPanel placeholders in a Blender’s Geometry Node provide a way to add inputs so that when inputs are iterated, they are iterated in a required order. Dynamically adding a new socket at a specific position isn’t straightforward
There doesn’t seem to be an easy way to link a Geometry Node input to a specific material setting (like say the
use_backface_culling)Using
Ctrlwhen placing nodes in the Geometry Nodes editor will ensure snapping to the grid and avoid node locations having floating point valuesBoth the Geometry Node editor and Composite editor have the same area type (
NODE_EDITOR), so the tree type in the context’s space data (context.space_data.tree_type) has to be used to distinguish between them (eg:CompositorNodeTreefor Composite editor)The only way to find nodes that belong to a particular frame is by checking the
parentproperty of all nodes. Alternatively node names can be saved in properties for direct access instead of full node traversal
Misc
bpy.msgbuscan be used to subscribe 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 works for all RNA properties (builtin ones and custom ones) and won’t work for ID propertiesAny object can be used as a owner for Blender’s
msgbussubscriptions usingbpy.msgbus.subscribe_rna. Using a owner allows to clear all subscriptions by the owner usingbpy.msgbus.clear_by_ownerBlender has a concept of Restricted Context during an extension’s
register()andunregister()methods that limits what is available during those methods. For example, thesceneentity does not exist inbpy.contextin these casesBlender’s
openvdbsupport usesnanogridto check and warn for leaks. Anyvdbgrids created must be ensured they are cleaned up to avoid memory errors, especially inpytestsBlender seems to have some wierd caching of contents loaded from
vdbfiles. Overwriting avdbfile that is already loaded and re-loading it as a new object will update the old object as wellThere is no separate font styling support in Blender (like
Bold,Italicetc) - the corresponding font file has to be loaded if that is needed (usingblf.load)Fonts loaded using
blf.loadhave to be unloaded usingblf.unloadwith the same font file as the param and not the returned font id from the loadFor 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 accordinglyA set of vertices can be turned to an object using
from_pydataofbpy.data.meshesthat can be used to create bounding box objectsbpy.utils.expose_bundled_modules()is needed to access Blender’s bundledopenvdbpackage in the background mode (bpy)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)Always use Blender’s portable installation to keep environments separate and clean
Never keep references to Blender data. This is the primary cause of most crashes as outlined in the gotchas