graphic:python:blender

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
    /Applications/Blender.app/Contents/MacOS/Blender
  • 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 os.name == "nt":
        os.system("cls") 
    else:
        os.system("clear") 

Common Operation

  • print to blender console (as default to print in system console)
        def bl_print(*data):
            # method ref: https://blender.stackexchange.com/questions/6173/where-does-console-output-go
            for window in bpy.context.window_manager.windows:
                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
    bpy.data.objects['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 bpy.data.objects.keys():
            bpy.data.objects.remove(bpy.data.objects[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 = bpy.app.binary_path
    task_path = os.path.join( os.path.dirname(__file__), 'process', 'task_make_preview.py' )
    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
    bpy.app.binary_path
  • get current scene file path
    import bpy
    file_path = bpy.data.filepath
    print(file_path)
  • file saved state
    file_saved = bpy.data.is_saved and not bpy.data.is_dirty
  • save file
    file="/tmp/test.blend"
    bpy.ops.wm.save_mainfile(filepath=file)
    bpy.ops.wm.open_mainfile(filepath=file)
     
    bpy.ops.wm.save_mainfile()
     
    # Save the scene as the new file
    bpy.ops.wm.save_as_mainfile(filepath=new_file_path)
  • 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)
        validate_path(config_path)
        return config_path
  • export file
    import bpy
    output_file = r"D:\zTmp\test.glb"
    bpy.ops.export_scene.gltf(
        filepath=output_file,
        export_format="GLB",
        export_apply=True,
        export_colors=False,
    )
  • list blender edit history
    bpy.context.window_manager.print_undo_steps()
  • find missing files and auto link in blender
    bpy.ops.file.find_missing_files(directory="D:\\Projects\\test-folder\\textures\\")
  • 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...
 
bpy.utils.register_class(MyCustomNode)
  • 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
    rgb
    ramp
    texture coordinate

ref: https://docs.blender.org/api/current/bpy.types.Object.html

  • object by name and its type, its parent, child
    bpy.data.objects['objName']
    bpy.data.objects['objName'].type
    
    bpy.data.objects['objName'].parent
    bpy.data.objects['objName'].children
    bpy.data.objects['objName'].children_recursive # all sub child
    bpy.data.objects['objName'].users_collection # collection it belong
  • object transform attribute value
    bpy.data.objects['objName'].location # not the global position, just its translate value
    bpy.data.objects['objName'].rotation_euler # rotation value in radian
    bpy.data.objects['objName'].rotation_euler # local scale
     
    bpy.data.objects['objName'].lock_location # array 3 bool, lock status of axis
    bpy.data.objects['objName'].lock_rotation
    bpy.data.objects['objName'].lock_scale
     
    bpy.data.objects['objName'].matrix_local.to_translation() # use matrix data, same as locaiton
    bpy.data.objects['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
    print(cur_obj['publish_size'])
  • 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)
    bpy.data.objects['objName'].dimensions
  • object display
    # get
    bpy.data.objects['objName'].display_type (BOUNDS, WIRE, SOLID, TEXTURED)
    # set
    bpy.data.objects['objName'].display_type='BOUNDS'
    bpy.data.objects['objName'].display_type='TEXTURED'
  • for empty objects (so called, group/null/locator), symbol size and shape
    obj.empty_display_size
    obj.empty_display_type # PLAIN_AXES, ARROWS, SINGLE_ARROW, CIRCLE, CUBE, SPHERE, CONE, IMAGE
  • 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_get(view_layer=None)
    obj.hide_set(state, view_layer=None)
    obj.visible_get(view_layer=None, viewport=None)
    obj.visible_in_viewport_get(viewport)
    
    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
    obj.select (bool, selected or not)
    obj.select_get(view_layer=None) # test select
    obj.select_set(state, view_layer=None)
    
    obj.data # its shape info, for mesh shape, it has mesh related info
    # ref: https://docs.blender.org/api/current/bpy.types.Mesh.html
    
    # other
    obj.name
    obj.tag
    obj.name_full
    vertexGroup
  • object selection
    bpy.ops.object.select_all(action='DESELECT') # select none
    obj.select_set(True) # select object add
    bpy.context.view_layer.objects.active = obj # set object as active
  • vertex
    obj.data.vertices
  • show all material and all texture
    import bpy
    import os
    for material in bpy.data.materials:
        print(material.name)
    for material in bpy.data.materials:
        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
                    print(file_path)
                    print(texture_res)
                    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 bpy.data.materials:
        print(material.name)
        if material.use_nodes:
            for node in material.node_tree.nodes:
                print(node)

How to use:

  • make a button in your custom addon
  • or paste to console panel of Blender
  • move object to floor
    import bpy
    context = bpy.context
     
    for obj in context.selected_objects:
        mx = obj.matrix_world
        minz = min((mx @ v.co)[2] for v in obj.data.vertices)
        mx.translation.z -= minz

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_space_type='VIEW_3D'
        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 cmds.ls(sl=1) 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 processScript.py
  • 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 "processThat.py"

    and python

    result = subprocess.check_output(["blender", "-b", "--python" "my_script.py", "--", "arg1", "arg2"], stderr=subprocess.STDOUT)
    print(result.decode())
  • 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.
    #your-script-output-here#
    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
    processBlend.py
    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
    try:
        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

ref:

  • 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
    • {SEPARATOR}
      • 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(
            name="email",
            description="User email",
            default=""
        )
    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
    C.window_manager.windows : windows
    C.window_manager.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
  • bpy.data (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)
      bpy.utils.register_class(MyDataItem)
       
      # 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()
      new_record.name = "roundA"
      new_record.value = 10.0

Property:

  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
    self.layout.prop(bpy.context.object,"myProp")
  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 bpy.context.object.bl_rna.properties["testEnum"].enum_items:
        print(item.name)

UI

  • 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: https://s-nako.work/2020/12/how-to-pass-arguments-to-custom-operator-in-blender-python/
    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_enum
      layout.operator_enum
      layout.prop_search : data, property, search_data, search_property, text
       
      layout.popover
       
      layout.tempalte_header() # insert common space header UI
      layout.template_ID() # para: data, property, 
      layout.template_ID_preview()
      ...
      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':
                break
        # 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
                break
        # 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
                else:
                    layout.label(text=rest_line_indent+chunk)
     
    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
    bpy.utils.register_class(MyPanel)
  • extra tips: remove VSCode Pylance “reportInvalidTypeForm” warn hightlight,
    • create a pyrightconfig.json with following config
      pyrightconfig.json
      {
          "reportInvalidTypeForm": false
      }
  • the addon folder, AddonName/init.py,
    print(__name__)
  • 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
    bpy.context.preferences.addons[__name__]
    bpy.context.preferences.addons[AddonName].preferences.attributeA
  • use PIL image and blender image
    # ref: https://blender.stackexchange.com/questions/270924/how-to-open-blenders-image-using-pillow
    # ref: https://stackoverflow.com/questions/21233946/pil-image-save-function-fails-in-blender-python
    import bpy
    import os
    import subprocess
    import sys
    import io
    import struct
    import numpy as np
     
    try:
        from PIL import Image
    except: 
        python_exe = os.path.join(sys.prefix, 'bin', 'python.exe')
        subprocess.call([python_exe, '-m', 'ensurepip'])
        subprocess.call([python_exe, '-m', 'pip', 'install', '--upgrade', 'pip'])
        subprocess.call([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 = bpy.data.images.new(name, 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(bpy.data.images["test"])
    im = im.transpose(Image.FLIP_LEFT_RIGHT).rotate(45)
    pil_to_image(im)
  • 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 Universal_Tool_template.py github
    SimpleLauncher.py
    # ref: https://blenderartists.org/t/parent-pyqt-window-widget-to-blenders-window/700722/2
     
    # install info
    # note: here we assume you already pip install PySide2 already, no check pyside2 code here
    bl_info={
        "name":"MyToolUI Launcher Addon",
        "description": "MyToolUI Launcher",
        "author": "YourName",
        "version": (1, 0, 0),
        "blender": (2, 80, 0),
        "location": "3D View > Tools",
        "category":"Development"
    }
     
    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()
            self.setLayout(main_layout)
     
            main_label = QtWidgets.QLabel("Example")
            main_input = QtWidgets.QLineEdit()
            main_button = QtWidgets.QPushButton("QButton to Toggle Always Top")
            main_layout.addWidget(main_label)
            main_layout.addWidget(main_input)
            main_layout.addWidget(main_button)
     
            main_button.clicked.connect(self.toggleAlwaysTopUI)
     
        def toggleAlwaysTopUI(self):
            self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowStaysOnTopHint)
            self.show()
     
    # 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
            layout.operator('custom.mytool_show_op')
     
    # 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):
            self.app = QtWidgets.QApplication.instance()
            if not self.app:
                self.app = 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
            self.widget.show()
            return {'FINISHED'}
     
    CLASSES = [
        MYTOOL_PT_MyTool_launcher_panel, 
        MYTOOL_OT_show_op
    ]
     
    def register():
        for cls in CLASSES:
            bpy.utils.register_class(cls)
     
    def unregister():
        for cls in CLASSES:
            bpy.utils.unregister_class(cls)
     
    if __name__ == "__main__":
        register()

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

My Blender addon template

import os
 
import bpy
from bpy.types import AddonPreferences, PropertyGroup, UIList, Operator
from bpy.props import (
    StringProperty,
    IntProperty,
    CollectionProperty,
    EnumProperty,
    PointerProperty,
)
 
bl_info = {
    "name": "UniversalTool",
    "description": "A demo template tool shows assets import and publish",
    "author": "Ying",
    "version": (1, 0, 0),
    "blender": (3, 2, 0),
    "location": "3D View > Tools",
    "category": "Object",
}
 
 
# =======================================
#  preference
# =======================================
class UniversalAddonPreferences(AddonPreferences):
    bl_idname = "UniversalTool"
 
    asset_author_name: StringProperty(
        name="Asset Author Name",
        default="",
        description="Enter the name of the asset author",
    )
 
    def draw(self, context):
        layout = self.layout
        layout.prop(self, "asset_author_name")
 
 
# =======================================
#  properties
# =======================================
class VariationItem(PropertyGroup):
    name: StringProperty(name="Name")
    variation_type: StringProperty(name="Type")
 
 
class VariationProps(PropertyGroup):
    variation_type_enum_items = [
        ("choice", "Visible Choice random", ""),
        ("option", "Visible Option random", ""),
        ("stack", "Visibile Stack end random", ""),
        ("linear", "Value Linear random", ""),
        ("step", "Value Step random", ""),
        ("material", "Value Material random", ""),
    ]
    variation_type: EnumProperty(
        items=variation_type_enum_items,
        name="Variation Type",
        description="Variation Type",
        default="choice",
    )
 
 
class PublishSetting(PropertyGroup):
    publish_type_enum_items = [
        ("new_asset", "New Asset", ""),
        ("update_asset", "Asset Update", ""),
    ]
    publish_name: StringProperty(
        name="Name", description="Asset name", default="", maxlen=1024
    )
    publish_version: IntProperty(
        name="Version", description="Version number.", default=1, min=1, max=9999
    )
    publish_dir: StringProperty(
        name="Publish Dir",
        description=("Asset Publish Directory."),
        subtype="DIR_PATH",
    )
    publish_type: EnumProperty(
        items=publish_type_enum_items,
        name="Publish Type",
        description="Publish Type",
        default="new_asset",
    )
 
 
# =======================================
#  operators
# =======================================
class AddCube(bpy.types.Operator):
    bl_idname = "custom.universal_add_cube"
    bl_label = "Simple Add Cube"
    bl_options = {"REGISTER", "UNDO"}  # Enable undo for this operator.
 
    def execute(self, context):
        bpy.ops.mesh.primitive_cube_add()
        return {"FINISHED"}
 
 
# example of AddObject operator class without UNIVERSAL_OT_AddObject naming pattern
class AddObject(Operator):
    bl_idname = "custom.universal_add_object"
    bl_label = "Add Object"
 
    def execute(self, context):
        obj = bpy.context.active_object
        wm = context.window_manager
        if obj:
            item = wm.variation_list.add()
            item.name = ",".join([x.name for x in context.selected_objects])
            item.variation_type = wm.variation_props.variation_type
        return {"FINISHED"}
 
 
class UNIVERSAL_OT_RemoveObject(Operator):
    bl_idname = "custom.universal_remove_object"
    bl_label = "Remove Object"
    bl_options = {"REGISTER"}
 
    @classmethod
    def poll(cls, context):
        wm = context.window_manager
        return len(wm.variation_list) > 0
 
    def execute(self, context):
        wm = context.window_manager
        idx = wm.variation_list_index
        wm.variation_list.remove(idx)
        return {"FINISHED"}
 
 
class PublishAsset(Operator):
    bl_idname = "custom.universal_publish_asset"
    bl_label = "Publish Asset"
 
    @classmethod
    def poll(self, context):
        wm = context.window_manager
        publish_prop = wm.publish_setting_props
        return (
            os.path.isdir(publish_prop.publish_dir)
            and publish_prop.publish_name.strip() != ""
        )
 
    def execute(self, context):
        wm = context.window_manager
        publish_prop = wm.publish_setting_props
        print(publish_prop.publish_dir)
        if publish_prop.publish_type != "new_asset":
            self.report({"ERROR"}, "Publish type must be new. (demo of error)")
            return {"FINISHED"}
        self.report({"INFO"}, "Publish successfully")
        return {"FINISHED"}
 
 
# =======================================
#  UI List
# =======================================
class UNIVERSAL_UL_VariationList(UIList):
    def draw_item(
        self, context, layout, data, item, icon, active_data, active_propname, index
    ):
        custom_icon = "BOOKMARKS"
        if self.layout_type in {"DEFAULT", "COMPACT"}:
            split = layout.split(factor=0.6)
            split.label(text=item.name, icon=custom_icon)
            split.label(text=item.variation_type)
        elif self.layout_type in {"GRID"}:
            layout.alignment = "CENTER"
            layout.label(text=item.name, icon=custom_icon)
            layout.label(text=item.variation_type)
 
 
# =======================================
#  panels
# =======================================
# UNIVERSAL_PT_VariationPanel: blender naming format
# VariationPanel: Python naming format results warning message
#    Warning: 'VariationPanel' does not contain '_PT_' with prefix and suffix
class UNIVERSAL_PT_VariationPanel(bpy.types.Panel):
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"
    bl_context = ""
    bl_category = "Universal"  # SIDE the tab name
    bl_label = "Universal Tool Panel"  # tab tab > panel name
 
    def draw(self, context):
        wm = context.window_manager
        layout = self.layout
        publish_prop = wm.publish_setting_props
 
        layout.operator("custom.universal_add_cube")
        layout.label(text="Variation Object List:", icon="EXPORT")
 
        row = layout.row()
        row.template_list(
            listtype_name="UNIVERSAL_UL_VariationList",
            list_id="",
            dataptr=wm,
            propname="variation_list",
            active_dataptr=wm,
            active_propname="variation_list_index",
        )
 
        layout.prop(wm.variation_props, "variation_type")
        row = layout.row()
        row.operator("custom.universal_add_object", icon="ADD", text="Add Object")
        row.operator(
            "custom.universal_remove_object", icon="REMOVE", text="Remove Object"
        )
 
        layout.prop(publish_prop, "publish_name", text="Super Name")
        layout.prop(publish_prop, "publish_version")
        layout.prop(publish_prop, "publish_dir")
        layout.prop(publish_prop, "publish_type", expand=True)
        asset_author_name = bpy.context.preferences.addons[
            "UniversalTool"
        ].preferences.asset_author_name.replace(" ", "-")
        if (
            os.path.isdir(publish_prop.publish_dir)
            and publish_prop.publish_name.strip() != ""
        ):
            result_file = os.path.join(
                publish_prop.publish_dir,
                publish_prop.publish_name.strip()
                + f"_v{publish_prop.publish_version}_{asset_author_name}.json",
            )
            layout.label(text=f"output: {result_file}")
        else:
            layout.label(text="output: not valid setting")
        layout.operator("custom.universal_publish_asset")
 
 
# =======================================
#  class list to register
# =======================================
classes = (
    UniversalAddonPreferences,
    # properties
    VariationItem,
    VariationProps,
    PublishSetting,
    # operators
    AddCube,
    AddObject,
    UNIVERSAL_OT_RemoveObject,
    PublishAsset,
    # ui
    UNIVERSAL_UL_VariationList,
    UNIVERSAL_PT_VariationPanel,
)
 
 
def register():
    for cls in classes:
        bpy.utils.register_class(cls)
    bpy.types.WindowManager.variation_list = CollectionProperty(type=VariationItem)
    bpy.types.WindowManager.variation_list_index = IntProperty(default=0)
    bpy.types.WindowManager.variation_props = PointerProperty(type=VariationProps)
    bpy.types.WindowManager.publish_setting_props = PointerProperty(type=PublishSetting)
 
 
def unregister():
    del bpy.types.WindowManager.variation_list
    del bpy.types.WindowManager.variation_list_index
    del bpy.types.WindowManager.variation_type_props
    del bpy.types.WindowManager.publish_setting_props
    for cls in classes:
        bpy.utils.unregister_class(cls)
 
 
if __name__ == "__main__":
    register()
  • graphic/python/blender.txt
  • Last modified: 2024/06/12 06:47
  • by ying