graphic:python:blender

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
Next revisionBoth sides next revision
graphic:python:blender [2023/11/15 01:30] – [File related Operation] yinggraphic:python:blender [2024/04/09 10:46] – [My related dev notes] ying
Line 38: Line 38:
     if each in bpy.data.objects.keys():     if each in bpy.data.objects.keys():
         bpy.data.objects.remove(bpy.data.objects[each], do_unlink=True)         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> </code>
  
Line 49: Line 67:
 </code> </code>
   * file saved state <code python>file_saved = bpy.data.is_saved and not bpy.data.is_dirty</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>   * get addon config path <code python>
 import bpy import bpy
Line 73: Line 101:
     export_colors=False,     export_colors=False,
 ) )
 +</code>
 +  * list blender edit history <code python>
 +bpy.context.window_manager.print_undo_steps()
 </code> </code>
 ===== App related Operation ===== ===== App related Operation =====
Line 362: Line 393:
 ref:  ref: 
   * https://wiki.blender.org/wiki/Reference/Release_Notes/2.80/Python_API/Addons   * 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://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 is: UPPER_CASE_{SEPARATOR}_mixed_case +  * **Class name convention**: UPPER_CASE_{SEPARATOR}_mixed_case 
-    * Header : _HT_ +    * UPPER_CASE: normally the ADDON_NAME, or a Unique ID_WORD that make it not crashing into other existing class names 
-    * Menu : _MT_ +    * {SEPARATOR} 
-    * Operator : _OT_ +      * Header : _HT_ 
-    * Panel : _PT_ +      * Menu : _MT_ 
-    * UIList : _UL_ +      * Operator : _OT_ 
-  * like  [A-Z][A-Z0-9_]*_MT_[A-Za-z0-9_]+ +      * Panel : _PT_ 
-  * if not follow above: you get warning "doesn't contain '_PT_' with prefix & suffix"+      * 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 ===== ===== common blender api commands =====
  
Line 495: Line 575:
 **UI** **UI**
   * bpy.types.Panel   * bpy.types.Panel
 +    * the panel display order is determined by the order bpy.utils.register_class processes
   * bpy.types.UIList   * bpy.types.UIList
   * bpy.types.Operator   * bpy.types.Operator
Line 553: Line 634:
 ... ...
 layout.template_list(type_name, list_id, ptr, propname, active, rows, maxrows, ..) # a list widget 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> </code>
 ===== Blender preference ===== ===== Blender preference =====
Line 571: Line 712:
 bpy.context.preferences.addons[__name__] bpy.context.preferences.addons[__name__]
 bpy.context.preferences.addons[AddonName].preferences.attributeA 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> </code>
  
Line 677: Line 871:
 | 2022-08-01 | study blender API (pc ready) | | 2022-08-01 | study blender API (pc ready) |
 | 2022-07-15 | go through standard blender develop workflow | | 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/05/15 04:30
  • by ying