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()
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)
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 is: UPPER_CASE_{SEPARATOR}_mixed_case
- Header : _HT_
- Menu : _MT_
- Operator : _OT_
- Panel : _PT_
- UIList : _UL_
- 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”
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
- 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
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