Output Console in Mac

  • default launch in mac app won't pop console window for script panel run output. run in cmd or shortcut to
  • the interactive console wont show output from script, only the interactive code

Output Console in Windows

  • window menu > toggle system console
  • to clear blender console
    import os
    if == "nt":

Common Operation

  • print to blender console (as default to print in system console)
        def bl_print(*data):
            # method ref:
            for window in
                screen = window.screen
                for area in screen.areas:
                    if area.type == 'CONSOLE':
                        override = {'window': window, 'screen': screen, 'area': area}
                        to_print = " ".join([str(x) for x in data])
                        bpy.ops.console.scrollback_append(override, text=to_print, type="OUTPUT") 
  • scale object['c1b_group'].scale=(1,1,1)
  • delete object
    import bpy
    floor_list = ['explosion_layer_0011_floor', 'guangzhou_layer_0010_floor', 'long_layer_0024_floor', 'thirteeen_layer_floor']
    sky_list = ['explosion_layer_0010_env_sky', 'guangzhou_layer_0010_sky', 'long_layer_0024_env_sky', 'thirteeen_layer_0035_env_sky']
    for each in floor_list+sky_list:
        if each in
  [each], do_unlink=True)
  • send blender file and python script to blender process with parameter
    # blender use empty scene to load task script with parameter after --, as (blender file, blender preview path, resolution)
    app_path =
    task_path = os.path.join( os.path.dirname(__file__), 'process', '' )
    info=subprocess.Popen(f'"{app_path}" --python "{task_path}" -- "{file_path}" "{blend_preview_dir}" 512 512',stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
    out, err = info.communicate()
  • task script reading parameter inside blender
    # task script
    import sys
    import bpy
    argv = sys.argv
    argv = argv[argv.index("--") + 1:]  # get all args after "--"
    print(argv)  # --> ['example', 'args', '123']
  • get current blender app path
  • get current scene file path
    import bpy
    file_path =
  • file saved state
    file_saved = and not
  • save file
    # Save the scene as the new file
  • get addon config path
    import bpy
    import os
    def validate_path(path):
        if not os.path.isdir(path):
            os.mkdir(path, mode=0o777)
    def get_config_path(package_name=None):
        if not package_name:
            package_name = __package__.lower()
        user_path = bpy.utils.resource_path('USER')
        config_path = os.path.join(user_path, "config")
        config_path = os.path.join(config_path, package_name)
        return config_path
  • export file
    import bpy
    output_file = r"D:\zTmp\test.glb"
  • list blender edit history
  • in blender, there are 2 attribute to check related to type
    • node.bl_idname: to refer node class, also for register custom node, “official” name of node class
    • node.type: the string id attribute to show the type
class MyCustomNode(bpy.types.Node):
    bl_idname = "MY_CUSTOM_NODE"
    # other properties and methods...
  • object.type will show the object type
    type MESH: Mesh
    type CURVE: Curve
    type SURFACE: Surface
    type META: Meta
    type FONT: Font
    type ARMATURE: Armature
    type LATTICE: Lattice
    type EMPTY: Empty - the locator/group/null object in other 3d app (but with more choice of looks)
    type GPENCIL: GPencil
    type CAMERA: Camera
    type LIGHT: Light
    type SPEAKER: Speaker
    type LIGHT_PROBE: Probe
  • material nodes
    principled BSDF
    mix shader
    HSV node
    bright contrast node
    texture coordinate


  • object by name and its type, its parent, child['objName']['objName'].type['objName'].parent['objName'].children['objName'].children_recursive # all sub child['objName'].users_collection # collection it belong
  • object transform attribute value['objName'].location # not the global position, just its translate value['objName'].rotation_euler # rotation value in radian['objName'].rotation_euler # local scale
['objName'].lock_location # array 3 bool, lock status of axis['objName'].lock_rotation['objName'].lock_scale
['objName'].matrix_local.to_translation() # use matrix data, same as locaiton['objName'].matrix_world.to_translation() # global translate, like xform -q -t -ws
  • object create/read custom property/attribute (like maya addAttr)
    C = bpy.context
    selected = C.selected_objects
    cur_obj = selected[0]
    cur_obj['publish_name'] = "color"
    cur_obj['publish_version'] = "1.1"
    cur_obj['publish_size'] = 100
    # it will auto detect and create with the correct data type
  • object get custom properties (user created attr)
    # please check blender python console, not blender main UI
    import bpy
    import os
    os.system('cls') # clear console history for easy see
    selected = bpy.context.selected_objects
    cur_obj = selected[0]
    for key, value in cur_obj.items():
        print(key, ":", value)
  • object bounding box (align with object pivot axis, follow its rotation), its value is its current scale bounding box size, rotation wont change its bounding box. (x,y,z is their height)['objName'].dimensions
  • object display
    # get['objName'].display_type (BOUNDS, WIRE, SOLID, TEXTURED)
    # set['objName'].display_type='BOUNDS'['objName'].display_type='TEXTURED'
  • for empty objects (so called, group/null/locator), symbol size and shape
  • object view/render property
    obj.hide_render # hide in render
    obj.hide_viewport # hide in render
    obj.hide_select # so-called template mode in maya, disable selection
    obj.hide_set(state, view_layer=None)
    obj.visible_get(view_layer=None, viewport=None)
    obj.ray_cast(origin, direction, distance=1.70141e+38, depsgraph=None) # test ray hit
    # Return (result, location, normal, index)
    obj.closest_point_on_mesh(origin, distance=1.84467e+19, depsgraph=None)
    # Return (result, location, normal, index)
  • object property and function list (bool, selected or not)
    obj.select_get(view_layer=None) # test select
    obj.select_set(state, view_layer=None) # its shape info, for mesh shape, it has mesh related info
    # ref:
    # other
  • object selection
    bpy.ops.object.select_all(action='DESELECT') # select none
    obj.select_set(True) # select object add = obj # set object as active
  • vertex
  • show all material and all texture
    import bpy
    import os
    for material in
    for material in
        if material.use_nodes:
            for node in material.node_tree.nodes:
                if node.type == 'TEX_IMAGE':
                    texture = node.image
                    texture_res = [ texture.size[0], texture.size[1] ]
                    file_path = texture.filepath
                    file_name = os.path.basename(file_path)
                    file_name_clean = file_name.rsplit('.',1)[0]
  • get all material's network
    import bpy
    import os
    for material in
        if material.use_nodes:
            for node in material.node_tree.nodes:

Native UI integration

  • Put on left side (hotkey 'T')
    class ToolLeftPanel(bpy.types.Panel):
        bl_label = 'ToolName'
        bl_idname = 'OBJECT_PT_ToolName'
        bl_space_type = 'VIEW_3D'
        bl_region_type = 'TOOLS'
        def draw(self, context):
            layout = self.layout
  • Put on right side (hotkey 'N')
    class ToolExamplePanel(bpy.types.Panel):
        bl_label = 'ExampleTitle'
        bl_idname = 'OBJECT_PT_ExampleTitle'
        bl_region_type = 'UI'
        bl_category = 'TabTitle'
        def draw(self, context):
            layout = self.layout

Blender Pipeline and Directory structure

Blender Addon development

  • Blender addon development is in Python
  • Blender addon is using its own UI system and UI concept
  • Blender API all under bpy, and its different API function is under its different sub category
    • bpy is like maya.cmds
    • like bpy.context for context related stuff. while cmds.xform(obj, t=(1,1,1,)) and for transform and list selected are all under same cmds.
    • bpy return objects, and you can get object data with it.
    • maya.cmds return strings, you call string name through functions to get data, like Maya cmds.getAttr(“myObj.tx”)
  • Blender self-defined commands are called Operator, it can be menu item, a button that link to a function
    • you can search menu Operator in “Edit menu > Menu Search (F3)”
    • you can also show all Operator in “Edit menu > Operator Search”, but you need go Edit > Preference > Display > show “Developer Extra”

Blender custom addon path and pipeline integration

  • if you don't want to put your addon in default blender addon path:
    %USERPROFILE%\AppData\Roaming\Blender Foundation\Blender\3.3\scripts\addons\
  • you can go Preference > File Paths: Data tab > Scripts = to define new scripts folder location, addons inside that scripts folder will be auto loaded as like default path.
  • ref:

integration into blender UI

reload blender addon

  • in Blender Preference > Addon, see your blender addon
  • uncheck your blender addon and re-check your blender addon to refresh your addon

Blender addon UI reference

Launch blender in its only console mode

  • run in cmd with python process
    blender.exe --background --python
  • run in cmd interactively
    blender.exe --background --python-console

    and python code

    cmd_list = [app_path, "--background", "--python-console"]
    subprocess.Popen(cmd_list, creationflags=subprocess.CREATE_NEW_CONSOLE) # new window
    # subprocess.check_call(cmd_list) # same window
  • process blend file in cmd
    # run a python string
    blender.exe test.blend --background --python-expr "import bpy;print( 'obj count:'+str(len(bpy.context.scene.objects)) )"
    # run through a python script
    blender.exe test.blend --background --python ""

    and python

    result = subprocess.check_output(["blender", "-b", "--python" "", "--", "arg1", "arg2"], stderr=subprocess.STDOUT)
  • run in cmd to render
    blender.exe --background test.blend --render-output d:/out --render-frame 1
  • blender cmd output and how to customize or remove certain out
    --- example out here ---
    Universal Material Map: Registered Converter classes.
    Blender 3.2.2 (hash bcfdb14560e7 built 2022-08-02 23:41:46)
    Read prefs: C:\Users\xxx\AppData\Roaming\Blender Foundation\Blender\3.2\config\userpref.blend
    Read blend: D:\test.blend
    Blender quit
    --- example out end ---
    use this additional flags in cmd after --background or -b
    --factory-startup will remove "Universal Material Map" and "Read prefs"
    -noexit will remove "Blender quit", also hangs blender there lol
  • example a script to be passed to blender for content reading with option to read arguments after “–” for python script
    import sys
    print('##xx## process start ##xx##') # output marker
    print('--option start--')
    # args = sys.argv[1:] # all blender arguments
    # Find the index of the "--" separator for script only arguments
        separator_index = sys.argv.index("--")
    except ValueError:
        separator_index = len(sys.argv)
    args = sys.argv[separator_index + 1:]
    for each in args:
        print('option: {0}'.format(each))
    print('--option end--')
    import bpy
    total_obj_count = len(bpy.context.scene.objects)
    print_count = 10
    if total_obj_count < 10:
        print_count = total_obj_count
    for i in range(print_count):
        print('object {0}/{1}: {2}'.format(i,total_obj_count,bpy.context.scene.objects[i].name))
    print('##xx## process end ##xx##') # output marker


  • Class name convention: UPPER_CASE_{SEPARATOR}_mixed_case
    • UPPER_CASE: normally the ADDON_NAME, or a Unique ID_WORD that make it not crashing into other existing class names
      • Header : _HT_
      • Menu : _MT_
      • Operator : _OT_
      • Panel : _PT_
      • UIList : _UL_
    • mixed_case
      • _CheckSelection (this more like standard python PEP class naming)
      • _Check_Selection
      • _check_selection
    • like [A-Z][A-Z0-9_]*_MT_[A-Za-z0-9_]+
    • if not follow above: you get warning “doesn't contain '_PT_' with prefix & suffix”
  • operator ID name convention (feel like a username for your operator inside blender )
    • category_name.operator_task_like_name
      • custom.addon_name_task_name (snake_case like PEP function)
    • For headers, menus and panels, the bl_idname is expected to match the class name
      • class: ADDON_NAME_OT_check_selection
      • bl_idname: addon_name.check_selection
  • other same as PEP
    • obj = “BigBox”
    • obj_max_count = 2
    • def function_name(arg_name)
    • class ClassName()

Example of class name, balancing between Python PEP

  • example addon,
    class BUILDMAKER_OT_CreateAsset(bpy.types.Operator):
        bl_idname = "custom.buildmaker_create_asset"
    class CreateAsset(bpy.types.Operator):
        bl_idname = "BUILDMAKER_OT_CreateAsset"
    class BUILDMAKER_OT_createAsset(bpy.types.Operator):
    # 2.8x onwards, property
    class MyOperator(bpy.types.Operator):
        value: IntProperty()
    class ToolProperties(bpy.types.PropertyGroup):
        commandLineMode: bpy.props.BoolProperty(
            name = "Command line mode",
            description = "The option specifies if the addon is used in the command line mode",
            default = False
    class ToolProps(bpy.types.PropertyGroup):
        email : StringProperty(
            description="User email",
    class TOOL_PROPS(bpy.types.PropertyGroup):
        lon: FloatProperty()
        lat: FloatProperty()
    class BUILDMAKER_PG_color(PropertyGroup):
        color: FloatVectorProperty(subtype='COLOR', min=0, max=1, update=updColor, size=4)
  • bpy.context or C for short, use for general blender query
    # global info
    C.engine: current render engine
    C.collection: default collection
    C.mode: current edit mode
    C.scene: current scene
    C.scene.objects: all objects of current scene
    C.screen: current screen : windows[0].screen.areas[0].type : "VIEW_3D"
    # selection and active object
    C.visible_objects: list of visible objects
    C.selected_objects: list of selected objects
    C.selected_editable_objects: list of selected objects that editable
    C.active_object: current object (last in selection)
    C.active_object.location : the translate attribute (Vector)
    C.selected_bones: list of selected bones
    C.active_bone: current bone
    C.objects_in_mode: list of objects in edit mode
  • (D for short) for scene data query, collection type (a detailed list with built-in .remove, .children, .objects)
    D.objects: all objects
    D.scenes: all scenes
    D.meshes: all meshes
  • bpy.types: built-in blender data types
  • bpy.ops: data operation commands,
    • mesh operation, selection conversion, uv and animation operation, like maya cmds.setKey, cmds.blendShape,
    • its sub-category for each set of commands, like bpy.ops.mesh.XXX, bpy.ops.object.XXX

related reading:

simple UI tutorial:

from bpy.props import

  • BoolProperty
  • FloatProperty (can have different unit as variation)
  • IntProperty (can have different unit as variation)
  • StringProperty (can be path, dir, byte, pass …)
  • PointerProperty(type=) (reference point of a object in scene, can be material or object or image…)
  • xxxVectorProperty - same as above but as a tuple of size=YouDefined-1-to-32
  • EnumProperty (drop down menu or toggle choice button, with id-name-descrip-icon-idx)
    • can have on-demand-item-list from items=YourGetItemFunc
  • CollectionProperty(type=InternalClassOrYourPropertyGroupClass)
    • it is a list of same property type, use template_list to show
    • add() > empty item, remove(index), clear
    • example
      # define MyDataItem property group for a data record
      class MyDataItem(bpy.types.PropertyGroup):
          name: bpy.props.StringProperty(default="")
          value: bpy.props.FloatProperty(default=0)
      # use collection to list a sheet of records for each object
      bpy.types.Object.my_item_list = bpy.props.CollectionProperty(type=MyDataItem)
      bpy.types.Object.my_item_list_index = bpy.props.IntProperty(name="idx", default=0)
      # add a empty record
      new_record = bpy.context.object.my_records .add() = "roundA"
      new_record.value = 10.0


  1. property can be inside a operator
    class OBJECT_OT_property_example(bpy.types.Operator):
        my_float: bpy.props.FloatProperty(name="length")
  2. property can be added to all object at class level
    bpy.types.Object.myProp = bpy.props.FloatProperty(default=10)
    # to show in addon panel
  3. property can have update=changeFunc call function to call once value change
  4. some property data are not directly read, like
    # define
    bpy.types.Object.testEnum = bpy.props.EnumProperty(items=[('option1','first',''),('option2','second','')])
    # access
    for item in["testEnum"].enum_items:


  • bpy.types.Panel
    • the panel display order is determined by the order bpy.utils.register_class processes
  • bpy.types.UIList
  • bpy.types.Operator
  • bpy.types.PropertyGroup (basically, a self defined property class with many properties from above, you have to define that inheritable class first)
  • prop inside operator
    # ref:
    class ADDMATRIX_add_cube(bpy.types.Operator):
        bl_idname = 'add_matrix_obj.add_cube'
        bl_label = "Add matrix cube"
        bl_options = {'REGISTER', "UNDO"}
        input1: bpy.props.IntProperty() # add argument1 as property "input1"
        input2: bpy.props.IntProperty() # add argument2 as property "input2"
        def execute(self, context):
            for xi in range(self.input1):
                x = xi*1.2
                for yi in range(self.input2):
                    y = yi*1.2
                    bpy.ops.mesh.primitive_cube_add(size=0.5, enter_editmode=False, align='WORLD', location=(x, y, 0))
            return {'FINISHED'}
  • radio button (it actually morph from a dropdown list)
    objectType_enum_items = (('0','Cube',''),('1','Pyramid',''))
    bpy.types.Scene.exmapleUI_obj_type = bpy.props.EnumProperty(items = objectType_enum_items )
    # draw function inside your UI panel
    def draw(self, context):
        layout = self.layout
        layout.label(text="Object Type")
        layout.prop(context.scene, 'exmapleUI_obj_type ', expand=True) # expand true make it from dropdown list into a toggle button
  • layout options
    • layout code
      layout = self.layout
      new_row = layout.row() # sub-layout, para: align, heading, 
      new_col = layout.column() # sub-layout, vertically, 
      new_col = layout.column_flow() # like column, but will flow
      new_grid = layout.grid_flow() # grid, para: row_major, columns, align
      new_split = layout.split() # para: factor, align
      layout.label(text="label text") # a text label ui
      layout.prop(prop_obj, "obj_property_name") # add property item, extra para: icon, expand,slider,toggle,icon_only
      layout.operator("operator_name") # add operator action button , extra para: icon
      layout.seperator(factor=1.0) # empty space
      layout.seperator_spacer() # horizontal space
      layout.prop_search : data, property, search_data, search_property, text
      layout.tempalte_header() # insert common space header UI
      layout.template_ID() # para: data, property, 
      layout.template_list(type_name, list_id, ptr, propname, active, rows, maxrows, ..) # a list widget
  • long label solution and auto wrap
    import bpy
    import textwrap
    long_text = """
            a long text  long text  long text
            b test
            c long text  long text  long text  long text  long text  long
            d text  long text  long text  long text """
    def get_max_label_width():
        # Get the 3D View area
        for area in bpy.context.screen.areas:
            if area.type == 'VIEW_3D':
        # Calculate the width of the panel
        panel_width = 280 # default value
        for region in area.regions:
            if region.type == 'UI':
                panel_width = region.width
                return panel_width
        # Calculate the maximum width of the label
        uifontscale = 9 * context.preferences.view.ui_scale
        max_label_width = int(panel_width // uifontscale)
        return max_label_width
    def create_long_label(layout, long_text, icon='',indent=''):
        max_label_width = get_max_label_width()
        first_icon_line_done = False
        rest_line_indent = indent
        if icon == '':
            first_icon_line_done = True
            rest_line_indent = ''
        # Split the text into lines and format each line
        for line in long_text.splitlines():
            # Remove leading and trailing whitespace
            line = line.strip()
            # Split the line into chunks that fit within the maximum label width
            for chunk in textwrap.wrap(line, width=max_label_width):
                if not first_icon_line_done:
                    layout.label(text=chunk, icon=icon)
                    first_icon_line_done = True
    class MyPanel(bpy.types.Panel):
        bl_idname = "OBJECT_PT_my_panel"
        bl_label = "My Panel"
        bl_space_type = "VIEW_3D"
        bl_region_type = "UI"
        def draw(self, context):
            layout = self.layout
            create_long_label(layout, long_text, "KEYTYPE_JITTER_VEC", '    ')
    # Register the panel
  • the addon folder, AddonName/,
  • create addon preference property and get its value
    # AddonName, __name__ will print the folder name
    class AddonNamePreferences(bpy.types.AddonPreferences):
        # the id of addon in preference
        bl_idname = __name__
        attributeA: StringProperty
        attributeB: StringProperty
    # it is also the name for bpy.context.preferences.addons[KEY] reference. just the name w.o version
  • use PIL image and blender image
    # ref:
    # ref:
    import bpy
    import os
    import subprocess
    import sys
    import io
    import struct
    import numpy as np
        from PIL import Image
        python_exe = os.path.join(sys.prefix, 'bin', 'python.exe')[python_exe, '-m', 'ensurepip'])[python_exe, '-m', 'pip', 'install', '--upgrade', 'pip'])[python_exe, '-m', 'pip', 'install', '--upgrade', 'pillow'])
    def pil_to_image(pil_image, name='NewImage'):
        width, height = pil_image.width, pil_image.height
        normalized = 1.0 / 255.0
        bpy_image =, width=width, height=height)
        bpy_image.pixels[:] = (np.asarray(pil_image.convert('RGBA'),dtype=np.float32) * normalized).ravel()
        return bpy_image
    def image_to_pil(bpy_image):
        img = bpy_image
        pixels = [int(px * 255) for px in bpy_image.pixels[:]]
        bytes = struct.pack("%sB" % len(pixels), *pixels)
        pil_image = Image.frombytes('RGBA', (bpy_image.size[0], bpy_image.size[1]), bytes)
        return pil_image
    im = image_to_pil(["test"])
    im = im.transpose(Image.FLIP_LEFT_RIGHT).rotate(45)
  • alternative
    bl_info = {
        "name": "My Test Addon",
        "category": "Object"}
    def register():
        import pip
        pip.main(['install', 'pillow'])
        from PIL import Image
    def unregister():
        print("Goodbye World")

Blender Addon to Launch QT Window

  • here is simple addon template to get both side working, for more complex setup, check my github
    # ref:
    # install info
    # note: here we assume you already pip install PySide2 already, no check pyside2 code here
        "name":"MyToolUI Launcher Addon",
        "description": "MyToolUI Launcher",
        "author": "YourName",
        "version": (1, 0, 0),
        "blender": (2, 80, 0),
        "location": "3D View > Tools",
    import bpy
    from PySide2 import QtWidgets, QtCore
    # optional single instant of MyToolUI
    single_MyToolUI = None
    # QT UI PART
    class MyToolUI(QtWidgets.QWidget):
        def __init__(self, parent=None):
            super(MyToolUI, self).__init__(parent)
            main_layout = QtWidgets.QVBoxLayout()
            main_label = QtWidgets.QLabel("Example")
            main_input = QtWidgets.QLineEdit()
            main_button = QtWidgets.QPushButton("QButton to Toggle Always Top")
        def toggleAlwaysTopUI(self):
            self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowStaysOnTopHint)
    # Blender UI PART
    class MYTOOL_PT_MyTool_launcher_panel(bpy.types.Panel):
        bl_space_type = 'VIEW_3D'
        bl_region_type = 'UI'
        bl_category = 'CoolTools' # the tab name
        bl_label = "MyTool Launcher" # tab tab > panel name
        def draw(self, context):
            layout = self.layout
    # Blender Operator Button to launch QT UI
    class MYTOOL_OT_show_op(bpy.types.Operator):
        bl_idname = 'custom.mytool_show_op'
        bl_label = "Launch MyTool"
        bl_options = {'REGISTER'}
        def execute(self, context):
   = QtWidgets.QApplication.instance()
            if not
       = QtWidgets.QApplication(['blender'])
            self.event_loop = QtCore.QEventLoop()
            # single UI
            global single_MyToolUI
            if single_MyToolUI is None:
                single_MyToolUI = MyToolUI()
            self.widget = single_MyToolUI
            return {'FINISHED'}
    CLASSES = [
    def register():
        for cls in CLASSES:
    def unregister():
        for cls in CLASSES:
    if __name__ == "__main__":

My related dev notes

2022-08-18 complete qt UI functions and workflow design todo: template integration, density check,
note system, shader lib, unreal asset conversion
2022-08-16 get blender to qt template working
2022-08-15 get pure blender addon working
2022-08-13 get blender addon template working
2022-08-12 complete blender UI system study
2022-08-08 study blender's concept of operator, panel
2022-08-01 study blender API (pc ready)
2022-07-15 go through standard blender develop workflow
  • graphic/python/blender.txt
  • Last modified: 2024/02/01 09:13
  • by ying