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
graphic:python:blender [2023/11/15 01:42] – [My related dev notes] yinggraphic:python:blender [2024/04/09 10:46] (current) – [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 685: Line 879:
     * (Darkfall studio - tut,shorts,anime) https://www.youtube.com/@DarkfallBlender     * (Darkfall studio - tut,shorts,anime) https://www.youtube.com/@DarkfallBlender
     * (Luca Chirichella - blender tut) https://www.youtube.com/@RedKProduction     * (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.1700012555.txt.gz
  • Last modified: 2023/11/15 01:42
  • by ying