====== 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 ======
{{:graphic:python:blender:blender_ui_integration_area.png?319|}}
* 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 ======
* 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: %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:
* 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 processblender.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
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:
* 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,
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)
* 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
# 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
* 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
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)
* 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
# 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)
===== 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 [[https://github.com/shiningdesign/universal_tool_template.py|Universal_Tool_template.py github]]
# 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
* 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 ======
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()