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)
cmd related operation
- 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']
File related Operation
- 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\\")
App related Operation
object type
- 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
object property
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
mesh object property
- vertex
obj.data.vertices
scene data operation
- 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)
object tool
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
- ui list (list widget like ui): https://sinestesia.co/blog/tutorials/using-uilists-in-blender/
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
Blender variable naming standards
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)
common blender api commands
- 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
Blender UI code
related reading:
- serpens - node based visual addon development tool (use node to generate addon panel + ui + load python code)
- blender built-in asset browser is a good UI design refer for asset browser
- blender official scripting tutorial:
simple UI tutorial:
- Create Property Group & Enumerator : https://www.youtube.com/watch?v=jZt3MO5D1R8
- Blender Addon - Python Programming: https://www.youtube.com/watch?v=zVtoEGQ7EA0&list=PLmGhGFnN_vMITtIKBKjPJ1h5C5i-iabqO
- PropertyGroup, CollectionProperty, bpy.types.UIList, template_list use 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
- blender property in details
- image showing all variation of a property:
Property:
- property can be inside a operator
class OBJECT_OT_property_example(bpy.types.Operator): my_float: bpy.props.FloatProperty(name="length")
- 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")
- (note: custom property not shown in ui unless accessed once), to force all object display the property:
from bpy.app.handlers import persistent @persistent def obj_init(scene): if hasattr(bpy.context, "object") and not bpy.context.object.myProp : bpy.context.object.myProp = None bpy.app.handlers.depsgraph_update_post.append(obj_init)
- property can have update=changeFunc call function to call once value change
- 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 }
Blender preference
- 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
3rd party library
- 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 |
- blender addon list and resource
- blender python tutorial
- (CG Python) https://www.youtube.com/@CGPython
- (Darkfall studio - tut,shorts,anime) https://www.youtube.com/@DarkfallBlender
- (Luca Chirichella - blender tut) https://www.youtube.com/@RedKProduction
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()