Show pageOld revisionsBacklinksBack to top This page is read only. You can view the source, but not change it. Ask your administrator if you think this is wrong. ====== 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 <code>/Applications/Blender.app/Contents/MacOS/Blender</code> * 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 <code python> import os if os.name == "nt": os.system("cls") else: os.system("clear") </code> ====== Common Operation ====== * print to blender console (as default to print in system console) <code python> 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") </code> * scale object <code> bpy.data.objects['c1b_group'].scale=(1,1,1) </code> * delete object <code python> 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) </code> ===== cmd related operation ===== * send blender file and python script to blender process with parameter <code python> # 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() </code> * task script reading parameter inside blender <code python> # task script import sys import bpy argv = sys.argv argv = argv[argv.index("--") + 1:] # get all args after "--" print(argv) # --> ['example', 'args', '123'] </code> ===== File related Operation ===== * get current blender app path <code python>bpy.app.binary_path</code> * get current scene file path<code python> import bpy file_path = bpy.data.filepath print(file_path) </code> * file saved state <code python>file_saved = bpy.data.is_saved and not bpy.data.is_dirty</code> * save file <code python> 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) </code> * get addon config path <code python> 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 </code> * export file <code python> 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, ) </code> * list blender edit history <code python> bpy.context.window_manager.print_undo_steps() </code> ===== 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 <code python> class MyCustomNode(bpy.types.Node): bl_idname = "MY_CUSTOM_NODE" # other properties and methods... bpy.utils.register_class(MyCustomNode) </code> * object.type will show the object type <code> 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 </code> * material nodes <code> principled BSDF mix shader HSV node bright contrast node rgb ramp texture coordinate </code> ===== object property ===== ref: https://docs.blender.org/api/current/bpy.types.Object.html * object by name and its type, its parent, child <code>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 </code> * object transform attribute value<code python> 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 </code> * object create/read custom property/attribute (like maya addAttr) <code python> 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']) </code> * object get custom properties (user created attr) <code python> # 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) </code> * 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) <code>bpy.data.objects['objName'].dimensions</code> * object display <code> # 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' </code> * for empty objects (so called, group/null/locator), symbol size and shape<code> obj.empty_display_size obj.empty_display_type # PLAIN_AXES, ARROWS, SINGLE_ARROW, CIRCLE, CUBE, SPHERE, CONE, IMAGE </code> * object view/render property <code> 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) </code> * object property and function list<code> 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 </code> * object selection <code python> 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 </code> ===== mesh object property ===== * vertex <code> obj.data.vertices </code> ===== scene data operation ===== * show all material and all texture <code python> 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] </code> * get all material's network <code python> 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) </code> ====== Native UI integration ====== {{:graphic:python:blender:blender_ui_integration_area.png?319|}} * Put on left side (hotkey 'T') <code python> 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 </code> * Put on right side (hotkey 'N') <code python> 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 </code> ====== Blender Pipeline and Directory structure ====== * ref: https://docs.blender.org/manual/en/latest/advanced/blender_directory_layout.html ====== 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: <code>%USERPROFILE%\AppData\Roaming\Blender Foundation\Blender\3.3\scripts\addons\</code> * 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: * https://docs.blender.org/manual/en/latest/editors/preferences/addons.html **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/ * https://levelup.gitconnected.com/build-a-blender-add-on-ready-to-scale-8c285f9f0a5 * https://blenderartists.org/t/parent-pyqt-window-widget-to-blenders-window/700722/2 * https://github.com/vfxpipeline/blender_pyside2_example **Launch blender in its only console mode** * run in cmd with python process<code>blender.exe --background --python processScript.py</code> * run in cmd interactively <code>blender.exe --background --python-console</code> and python code <code python> 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 </code> * process blend file in cmd <code> # 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" </code> and python <code python> result = subprocess.check_output(["blender", "-b", "--python" "my_script.py", "--", "arg1", "arg2"], stderr=subprocess.STDOUT) print(result.decode()) </code> * run in cmd to render <code> blender.exe --background test.blend --render-output d:/out --render-frame 1 </code> * blender cmd output and how to customize or remove certain out <code> --- 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 </code> * example a script to be passed to blender for content reading with option to read arguments after "--" for python script <code python 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 </code> ===== Blender variable naming standards ===== ref: * https://wiki.blender.org/wiki/Reference/Release_Notes/2.80/Python_API/Addons * https://developer.blender.org/docs/release_notes/2.80/python_api/addons/ * https://s-nako.work/2020/12/blender-addon-naming-rules/ * https://b3d.interplanety.org/en/class-naming-conventions-in-blender-2-8-python-api/ * **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, <code python> 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) </code> ===== common blender api commands ===== * bpy.context or C for short, use for general blender query <code python> # 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 </code> * bpy.data (D for short) for scene data query, collection type (a detailed list with built-in .remove, .children, .objects) <code python> D.objects: all objects D.scenes: all scenes D.meshes: all meshes </code> * 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) * https://blendermarket.com/products/serpens * https://joshuaknauber.gumroad.com/l/serpens-b3d * blender built-in asset browser is a good UI design refer for asset browser * blender official scripting tutorial: * https://www.youtube.com/playlist?list=PLa1F2ddGya_8acrgoQr1fTeIuQtkSd6BW * https://studio.blender.org/training/scripting-for-artists/chapter/top-scripting-for-artists/ 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: * https://sinestesia.co/blog/tutorials/using-uilists-in-blender/ **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 <code python> # 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 </code> * blender property in details * https://harlepengren.com/understanding-how-to-use-blender-python-properties/ * image showing all variation of a property: * https://s-nako.work/2020/12/list-of-blender-properties/ Property: - property can be inside a operator <code python> class OBJECT_OT_property_example(bpy.types.Operator): my_float: bpy.props.FloatProperty(name="length") </code> - property can be added to all object at class level <code python> bpy.types.Object.myProp = bpy.props.FloatProperty(default=10) # to show in addon panel self.layout.prop(bpy.context.object,"myProp") </code> * (note: custom property not shown in ui unless accessed once), to force all object display the property: <code python> 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)</code> * https://s-nako.work/2020/12/how-to-display-custom-property-on-properties-editor-in-blender-addon/ * https://docs.blender.org/api/current/bpy.app.handlers.html - property can have update=changeFunc call function to call once value change - some property data are not directly read, like <code python> # 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) </code> **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 <code python># 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'} </code> * radio button (it actually morph from a dropdown list) <code python> 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 </code> * layout options * layout code <code python> 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 </code> * long label solution and auto wrap <code python> 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) </code> ===== Blender preference ===== * the addon folder, AddonName/__init__.py, <code> print(__name__) </code> * create addon preference property and get its value <code python> # 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 </code> ===== 3rd party library ===== * use PIL image and blender image <code python> # 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) </code> * alternative <code python> 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") </code> ====== Blender Addon to Launch QT Window ====== * here is simple addon template to get both side working, for more complex setup, check my [[https://github.com/shiningdesign/universal_tool_template.py|Universal_Tool_template.py github]] <code python 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() </code> ====== 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 * https://github.com/agmmnn/awesome-blender * 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 ====== <code python> 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() </code> graphic/python/blender.txt Last modified: 2024/04/09 10:46by ying