├── .gitignore ├── LICENSE ├── README.md ├── ch1 └── OurFirstScript.py ├── ch10 └── addons │ └── punchclock.py ├── ch11 └── addons │ └── latte_express.py ├── ch12 ├── _media_ │ └── textures │ │ ├── bricks_ao.png │ │ ├── bricks_baseColor.png │ │ ├── bricks_height.png │ │ ├── bricks_normal.png │ │ └── bricks_roughness.png └── addons │ └── textament.py ├── ch2 ├── 0300_accessing_blender_modules.py ├── 0400_accessing_blender_data.py └── 0500_understanding_user_context.py ├── ch3 ├── addons │ └── object_collector.py └── the_simplest_add_on.py ├── ch4 └── addons │ └── object_floor_transform.py ├── ch5 └── addons │ ├── icon_smile_64.png │ └── very_simple_panel.py ├── ch6 └── addons │ └── structured_addon │ ├── __init__.py │ ├── _refresh_.py │ ├── img_loader.py │ ├── objects_panel.py │ ├── operators.py │ ├── panel.py │ ├── pictures │ ├── pack_64.png │ └── smile_64.png │ └── preferences.py ├── ch7 └── addons │ ├── action_to_range.py │ └── vert_runner.py ├── ch8 ├── _scenes_ │ └── ani_loop.blend └── addons │ └── object_shaker.py └── ch9 └── addons └── pendulum.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python-Scripting-in-Blender 2 | 3 | 4 | 5 | This is the code repository for [Python Scripting in Blender](https://www.packtpub.com/product/python-scripting-in-blender-3x/9781803234229?utm_source=github&utm_medium=repository&utm_campaign=9781803235851), published by Packt. 6 | 7 | **Extend the power of Blender using Python to create objects, animations, and effective add-ons** 8 | 9 | ## What is this book about? 10 | Blender, a powerful open source 3D software, can be extended and powered up using the Python programming language. This book teaches you how to automate laborious operations using scripts, and expand the set of available commands, graphic interfaces, tools, and event responses, which will enable you to add custom features to meet your needs and bring your creative ideas to life. 11 | 12 | This book covers the following exciting features: 13 | * Understand the principles of 3D and programming, and learn how they operate in Blender 14 | * Build engaging and navigation-friendly user interfaces that integrate with the native look and feel 15 | * Respect coding guidelines and deliver readable and compliant code without the loss of originality 16 | * Package your extensions into a complete add-on, ready for installation and distribution 17 | * Create interactive tools with a direct response to the user's action 18 | * Code comfortably and safely using version control 19 | 20 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/1803234229) today! 21 | 22 | https://www.packtpub.com/ 24 | 25 | ## Instructions and Navigations 26 | All of the code is organized into folders. For example, Chapter02. 27 | 28 | The code will look like the following: 29 | ``` 30 | bl_info = { 31 | "name": "Object Shaker", 32 | "author": "Packt Man", 33 | "version": (1, 0), 34 | "blender": (3, 00, 0), 35 | "description": "Add Shaky motion to active object", 36 | "location": "Object Right Click -> Add Object Shake", 37 | "category": "Learning", 38 | } 39 | ``` 40 | 41 | **Following is what you need for this book:** 42 | This book is for Blender users who want to expand their skills and learn scripting, technical directors looking to automate laborious tasks, and professionals and hobbyists who want to learn more about the Python architecture underlying the Blender interface. Prior experience with Blender is a prerequisite, along with a basic understanding of the Python syntax—however, the book does provide quick explanations to bridge potential gaps in your background knowledge. 43 | 44 | With the following software and hardware list you can run all code files present in the book (Chapter 1-12). 45 | ### Software and Hardware List 46 | | Chapter | Software required | OS required | 47 | | -------- | ------------------------------------ | ----------------------------------- | 48 | | 1-12 | Blender 3.3 | Windows, Mac OS X, and Linux (Any) | 49 | | 1-12 | Visual Studio Code 1.70 or later | Windows, Mac OS X, and Linux (Any) | 50 | 51 | We also provide a PDF file that has color images of the screenshots/diagrams used in this book. [Click here to download it](https://packt.link/G1mMt). 52 | 53 | ### Related products 54 | * Blender 3D Incredible Models [[Packt]](https://www.packtpub.com/product/blender-3d-incredible-models/9781801817813?utm_source=github&utm_medium=repository&utm_campaign=9781801817813) [[Amazon]](https://www.amazon.com/dp/B0B1QMV8LR) 55 | 56 | * Squeaky Clean Topology in Blender [[Packt]](https://www.packtpub.com/product/squeaky-clean-topology-in-blender/9781803244082?utm_source=github&utm_medium=repository&utm_campaign=9781803244082) [[Amazon]](https://www.amazon.com/dp/1803244089) 57 | 58 | ## Errata 59 | * Page 45 (second last code block): 60 | ``` 61 | import bpy 62 | for ob in bpy.context.selected_objects: 63 | ob.select_set(False) 64 | ``` 65 | **_should be_** 66 | ``` 67 | import bpy 68 | for ob in bpy.context.selected_objects: 69 | ob.select_set(False) 70 | ``` 71 | 72 | ## Get to Know the Author 73 | **Paolo Acampora** is a software developer at Binary Alchemy and a veteran technical director for animation, visual effects, and prototyping. He is a long-time Blender user and advocates for the widespread adoption of open source software and code literacy. 74 | He works with studios to kickstart their computer graphics pipelines and shares his tools with the Blender community. 75 | -------------------------------------------------------------------------------- /ch1/OurFirstScript.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy import data as D 3 | from bpy import context as C 4 | from mathutils import * 5 | from math import * 6 | 7 | #~ PYTHON INTERACTIVE CONSOLE 3.9.7 (default, Oct 11 2021, 19:31:28) [MSC v.1916 64 bit (AMD64)] 8 | #~ 9 | #~ Builtin Modules: bpy, bpy.data, bpy.ops, bpy.props, bpy.types, bpy.context, bpy.utils, bgl, blf, mathutils 10 | #~ Convenience Imports: from mathutils import *; from math import * 11 | #~ Convenience Variables: C = bpy.context, D = bpy.data 12 | #~ 13 | bpy.ops.object.delete(use_global=False) 14 | #~ {'FINISHED'} 15 | #~ 16 | 17 | bpy.ops.mesh.primitive_cylinder_add(radius=1, depth=2, enter_editmode=False, align='WORLD', location=(0, 0, 0), scale=(1, 1, 1)) 18 | #~ {'FINISHED'} 19 | #~ 20 | 21 | bpy.ops.mesh.primitive_uv_sphere_add(radius=1, enter_editmode=False, align='WORLD', location=(0, 0, 2), scale=(1, 1, 1)) 22 | #~ {'FINISHED'} 23 | #~ 24 | -------------------------------------------------------------------------------- /ch10/addons/punchclock.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "PunchClock Text", 3 | "author": "Packt Man", 4 | "version": (1, 0), 5 | "blender": (3, 00, 0), 6 | "description": "Create a Hour/Minutes text object", 7 | "category": "Learning", 8 | } 9 | 10 | 11 | import bpy 12 | import datetime 13 | 14 | 15 | class PunchClock(bpy.types.Operator): 16 | """Create Hour/Minutes text""" 17 | bl_idname = "text.punch_clock" 18 | bl_label = "Create Hour/Minutes Text" 19 | bl_description = "Create Hour Minutes Text" 20 | bl_options = {'REGISTER', 'UNDO'} 21 | 22 | hour: bpy.props.IntProperty(default=0, min=0, max=23) 23 | mins: bpy.props.IntProperty(default=0, min=0, max=59) 24 | set_hours: bpy.props.BoolProperty(default=True) 25 | 26 | @classmethod 27 | def poll(cls, context): 28 | return context.mode == 'OBJECT' 29 | 30 | def draw(self, context): 31 | layout = self.layout 32 | 33 | row = layout.row(align=True) 34 | row.alignment = 'CENTER' 35 | 36 | row.prop(self, 'hour', text="") 37 | row.label(text=' :',) 38 | row.prop(self, 'mins', text="") 39 | 40 | def invoke(self, context, event): 41 | now = datetime.datetime.now() 42 | 43 | self.hour = now.hour 44 | self.mins = now.minute 45 | 46 | self.txt_crv = bpy.data.curves.new(type="FONT", name="TXT-hourmin") 47 | self.txt_obj = bpy.data.objects.new(name="Font Object", object_data=self.txt_crv) 48 | context.collection.objects.link(self.txt_obj) 49 | 50 | context.window_manager.modal_handler_add(self) 51 | return {'RUNNING_MODAL'} 52 | 53 | @staticmethod 54 | def range_loop(value, v_min, v_max): 55 | if value < v_min: 56 | return v_max 57 | 58 | if value > v_max: 59 | return v_min 60 | 61 | return value 62 | 63 | def modal(self, context, event): 64 | # https://docs.blender.org/api/3.3/bpy_types_enum_items/event_type_items.html 65 | 66 | if event.type == 'MOUSEMOVE': 67 | delta = event.mouse_x - event. mouse_prev_x 68 | delta /= 10 69 | delta = round(delta) 70 | 71 | if self.set_hours: 72 | self.hour += delta 73 | else: 74 | self.mins += delta 75 | 76 | self.txt_crv.body = f"{self.hour:02}:{self.mins:02}" 77 | 78 | elif event.type == 'RET': 79 | return {'FINISHED'} 80 | if event.type == 'TAB' and event.value == 'PRESS': 81 | self.set_hours = False if self.set_hours else True 82 | elif event.type == 'LEFTMOUSE' and event.value == 'PRESS': 83 | if not self.set_hours: 84 | return {'FINISHED'} 85 | 86 | self.set_hours = False 87 | 88 | elif event.type in {'RIGHTMOUSE', 'ESC'}: 89 | bpy.data.objects.remove(self.txt_obj) 90 | return {'CANCELLED'} 91 | 92 | return {'RUNNING_MODAL'} 93 | 94 | def execute(self, context): 95 | txt_crv = bpy.data.curves.new(type="FONT", name="TXT-hourmin") 96 | txt_crv.body = f"{int(self.hour):02}:{int(self.mins):02}" 97 | txt_obj = bpy.data.objects.new(name="Font Object", object_data=txt_crv) 98 | context.collection.objects.link(txt_obj) 99 | 100 | return {'FINISHED'} 101 | 102 | 103 | 104 | 105 | def menu_func(self, context): 106 | self.layout.separator() 107 | row = self.layout.row() 108 | row.operator_context = "INVOKE_DEFAULT" 109 | row.operator(PunchClock.bl_idname, icon='TIME') 110 | 111 | 112 | def register(): 113 | bpy.utils.register_class(PunchClock) 114 | bpy.types.VIEW3D_MT_add.append(menu_func) 115 | 116 | 117 | def unregister(): 118 | bpy.types.VIEW3D_MT_add.remove(menu_func) 119 | bpy.utils.unregister_class(PunchClock) 120 | -------------------------------------------------------------------------------- /ch11/addons/latte_express.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "Latte Express", 4 | "author": "Packt Man", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "description": "Create a Lattice on the active object", 8 | "category": "Learning", 9 | } 10 | 11 | 12 | import bpy 13 | from mathutils import Vector 14 | 15 | 16 | class LatteExpress(bpy.types.Operator): 17 | """Tooltip""" 18 | bl_idname = "object.latte_express" 19 | bl_label = "Create Lattice on active object" 20 | bl_options = {'REGISTER', 'UNDO'} 21 | 22 | add_subsurf: bpy.props.BoolProperty(default=True) 23 | subd_levels: bpy.props.IntProperty(default=2) 24 | grid_levels: bpy.props.IntVectorProperty(default=(3, 3, 3), min=1, subtype='XYZ') 25 | add_armature: bpy.props.BoolProperty(default=True) 26 | 27 | @classmethod 28 | def poll(cls, context): 29 | return context.active_object 30 | 31 | def execute(self, context): 32 | ob = context.object 33 | 34 | if self.add_subsurf: 35 | subdiv = ob.modifiers.new('Subdivision', 'SUBSURF') 36 | subdiv.levels = self.subd_levels 37 | subdiv.render_levels = self.subd_levels 38 | subdiv.subdivision_type = 'SIMPLE' 39 | 40 | latt_data = bpy.data.lattices.new(f"LAT-{ob.name}") 41 | latt_obj = bpy.data.objects.new(name=latt_data.name, object_data=latt_data) 42 | 43 | latt_data.points_u = self.grid_levels[0] 44 | latt_data.points_v = self.grid_levels[1] 45 | latt_data.points_w = self.grid_levels[2] 46 | 47 | latt_data.use_outside = True 48 | 49 | context.collection.objects.link(latt_obj) 50 | latt_obj.scale = ob.dimensions 51 | 52 | btm_left = min((c for c in ob.bound_box), key=sum) 53 | top_right = max((c for c in ob.bound_box), key=sum) 54 | 55 | btm_left = Vector(btm_left) 56 | ob_center = btm_left.lerp(top_right, 0.5) 57 | 58 | ob_translation = ob.matrix_world.to_translation() 59 | ob_translation += ob_center 60 | 61 | if not self.add_armature: 62 | latt_obj.location = ob_translation 63 | else: 64 | arm_data = bpy.data.armatures.new(f"ARM-{ob.name}") 65 | arm_obj = bpy.data.objects.new(name=arm_data.name, object_data=arm_data) 66 | context.collection.objects.link(arm_obj) 67 | 68 | latt_obj.parent = arm_obj 69 | arm_obj.location = ob_translation 70 | 71 | half_height = ob.dimensions[2]/2 72 | arm_obj.location[2] -= half_height 73 | latt_obj.location[2] += half_height 74 | 75 | context.view_layer.objects.active = arm_obj 76 | bpy.ops.object.mode_set(mode='EDIT', toggle=False) 77 | 78 | grid_levels = self.grid_levels[2] 79 | bone_length = ob.dimensions[2] / (grid_levels - 1) 80 | 81 | for i in range(grid_levels): 82 | eb = arm_data.edit_bones.new(f"LAT_{i:02}") 83 | 84 | eb.head = (0, 0, i * bone_length) 85 | eb.tail = (0, 0, eb.head[2] + bone_length) 86 | 87 | rel_height = i / (grid_levels - 1) 88 | rel_height -= 0.5 89 | 90 | vert_ids = [] 91 | 92 | for id, p in enumerate(latt_data.points): 93 | p_height = p.co[2] 94 | 95 | if p_height == rel_height: 96 | vert_ids.append(id) 97 | 98 | vg = latt_obj.vertex_groups.new(name=eb.name) 99 | vg.add(vert_ids, 1.0, 'REPLACE') 100 | 101 | arm_mod = latt_obj.modifiers.new("Armature", "ARMATURE") 102 | arm_mod.object = arm_obj 103 | 104 | bpy.ops.object.mode_set(mode='POSE', toggle=False) 105 | 106 | # Create Widget 107 | v_cos = [ 108 | [-0.5, 0.0, -0.5], 109 | [-0.5, 0.0, 0.5], 110 | [0.5, 0.0, 0.5], 111 | [0.5, 0.0, -0.5] 112 | ] 113 | 114 | edges = [ 115 | [0, 1], [1, 2], [2, 3], [3, 0] 116 | ] 117 | 118 | mesh = bpy.data.meshes.new("WDG-square") 119 | mesh.from_pydata(v_cos, edges, []) 120 | wdg_obj = bpy.data.objects.new(mesh.name, mesh) 121 | context.collection.objects.link(wdg_obj) 122 | 123 | for pb in arm_obj.pose.bones: 124 | pb.custom_shape = wdg_obj 125 | pb.custom_shape_scale_xyz[0] = ob.dimensions[0] / bone_length 126 | pb.custom_shape_scale_xyz[2] = ob.dimensions[1] / bone_length 127 | 128 | wdg_obj.hide_set(True) 129 | latt_obj.hide_set(True) 130 | 131 | mod = ob.modifiers.new("Lattice", "LATTICE") 132 | mod.object = latt_obj 133 | ob.select_set(False) 134 | 135 | return {'FINISHED'} 136 | 137 | 138 | def menu_func(self, context): 139 | self.layout.operator(LatteExpress.bl_idname, 140 | icon="MOD_LATTICE") 141 | 142 | 143 | def register(): 144 | bpy.utils.register_class(LatteExpress) 145 | bpy.types.VIEW3D_MT_object_context_menu.append(menu_func) 146 | 147 | def unregister(): 148 | bpy.utils.unregister_class(LatteExpress) 149 | bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func) 150 | -------------------------------------------------------------------------------- /ch12/_media_/textures/bricks_ao.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Scripting-in-Blender/3e0c7afc69c783c9cc6c57abce3b284d1395d044/ch12/_media_/textures/bricks_ao.png -------------------------------------------------------------------------------- /ch12/_media_/textures/bricks_baseColor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Scripting-in-Blender/3e0c7afc69c783c9cc6c57abce3b284d1395d044/ch12/_media_/textures/bricks_baseColor.png -------------------------------------------------------------------------------- /ch12/_media_/textures/bricks_height.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Scripting-in-Blender/3e0c7afc69c783c9cc6c57abce3b284d1395d044/ch12/_media_/textures/bricks_height.png -------------------------------------------------------------------------------- /ch12/_media_/textures/bricks_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Scripting-in-Blender/3e0c7afc69c783c9cc6c57abce3b284d1395d044/ch12/_media_/textures/bricks_normal.png -------------------------------------------------------------------------------- /ch12/_media_/textures/bricks_roughness.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Scripting-in-Blender/3e0c7afc69c783c9cc6c57abce3b284d1395d044/ch12/_media_/textures/bricks_roughness.png -------------------------------------------------------------------------------- /ch12/addons/textament.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "Textament", 4 | "author": "Packt Man", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "description": "Load and connect node textures", 8 | "location": "Node-Graph header", 9 | "category": "Learning" 10 | } 11 | 12 | 13 | import os 14 | import bpy 15 | from bpy_extras.io_utils import ImportHelper 16 | 17 | 18 | class AddTextures(bpy.types.Operator, ImportHelper): 19 | """Load and connect material textures""" 20 | bl_idname = "texture.textament_load" 21 | bl_label = "Load and connect textures" 22 | bl_description = "Load and connect material textures" 23 | 24 | _spacing = 340 25 | 26 | directory: bpy.props.StringProperty() 27 | 28 | filter_glob: bpy.props.StringProperty(default="*.png; *.jpg", options={'HIDDEN'}) 29 | 30 | files: bpy.props.CollectionProperty( 31 | name="File Path", 32 | type=bpy.types.OperatorFileListElement, 33 | ) 34 | 35 | @classmethod 36 | def poll(cls, context): 37 | ob = context.object 38 | if not ob: 39 | return False 40 | 41 | mat = ob.active_material 42 | if not ob: 43 | return False 44 | 45 | tree = mat.node_tree 46 | if not tree: 47 | return False 48 | 49 | return tree.nodes.active 50 | 51 | def execute(self, context): 52 | mat = context.object.active_material 53 | target_node = mat.node_tree.nodes.active 54 | 55 | match_rule = lambda x: x.lower().replace(" ", "") 56 | input_names = target_node.inputs.keys() 57 | 58 | matching_names = {} 59 | 60 | for f in self.files: 61 | for inp in input_names: 62 | if match_rule(inp) in match_rule(f.name): 63 | matching_names[inp] = f.name 64 | break 65 | 66 | sorted_inputs = [i for i in input_names if i in matching_names] 67 | 68 | for i, inp in enumerate(sorted_inputs): 69 | img_path = os.path.join(self.directory, matching_names[inp]) 70 | img = bpy.data.images.load(img_path, check_existing=True) 71 | 72 | if target_node.inputs[inp].type != 'RGBA': 73 | img.colorspace_settings.name = 'Non-Color' 74 | 75 | img_node = mat.node_tree.nodes.new("ShaderNodeTexImage") 76 | img_node.image = img 77 | 78 | img_node.location = target_node.location 79 | img_node.location.x -= self._spacing 80 | img_node.location.y -= i * self._spacing 81 | 82 | if inp == "Base Color": 83 | mix = mat.node_tree.nodes.new("ShaderNodeMixRGB") 84 | mix.location = img_node.location 85 | 86 | img_node.location.x -= self._spacing / 2 87 | mix.location.x += self._spacing / 2 88 | 89 | mat.node_tree.links.new(img_node.outputs["Color"], mix.inputs["Color1"]) 90 | img_node = mix 91 | 92 | if inp != "Normal": 93 | mat.node_tree.links.new(img_node.outputs["Color"], target_node.inputs[inp]) 94 | continue 95 | 96 | normal_map = mat.node_tree.nodes.new("ShaderNodeNormalMap") 97 | normal_map.location = img_node.location 98 | 99 | img_node.location.x -= self._spacing / 2 100 | normal_map.location.x += self._spacing / 2 101 | 102 | mat.node_tree.links.new(img_node.outputs["Color"], normal_map.inputs["Color"]) 103 | mat.node_tree.links.new(normal_map.outputs["Normal"], target_node.inputs["Normal"]) 104 | 105 | return {'FINISHED'} 106 | 107 | 108 | def shader_header_button(self, context): 109 | layout = self.layout 110 | layout.operator(AddTextures.bl_idname, icon="NODE_TEXTURE", text="Load Textures") 111 | 112 | 113 | def register(): 114 | bpy.utils.register_class(AddTextures) 115 | bpy.types.NODE_HT_header.append(shader_header_button) 116 | 117 | 118 | def unregister(): 119 | bpy.types.NODE_HT_header.remove(shader_header_button) 120 | bpy.utils.unregiser_class(AddTextures) 121 | -------------------------------------------------------------------------------- /ch2/0300_accessing_blender_modules.py: -------------------------------------------------------------------------------- 1 | 2 | # The following code imports: 3 | # 4 | # - the main blender functionalities, that is, bpy. 5 | # - bpy.data for scene and object access, and assigns it the shortcut D 6 | # - bpy.context for enquiring the scene status, and assigns it the shortcut C 7 | # 8 | # This code is executed automatically in blender's interactive console when it starts 9 | # 10 | 11 | import bpy 12 | from bpy import data as D 13 | from bpy import context as C 14 | 15 | 16 | # This line queries the number of objects stored in blender. The expected result 17 | # if it is typed in the interactive console of blender's default scene is 3. 18 | 19 | len(bpy.data.objects) 20 | 21 | 22 | # This line gives access to the first object created in the current session. 23 | # The expected result if it is typed in the interactive console of blender's 24 | # default scene is bpy.data.objects['Camera'] 25 | 26 | bpy.data.objects[0] 27 | 28 | 29 | # The following snippet prints out the name and type of the objects currently 30 | # stored in blender. The expected results if typed in the default scene is 31 | # 32 | # Camera CAMERA 33 | # Cube MESH 34 | # Light LIGHT 35 | 36 | import bpy 37 | for ob in bpy.data.objects: 38 | print(ob.name, ob.type) 39 | 40 | 41 | # The following snippet prints out the index, name and type of the objects 42 | # currently stored in blender. The expected results if typed in the default 43 | # scene's interactive console is 44 | # 45 | # 0 Camera CAMERA 46 | # 1 Cube MESH 47 | # 2 Light LIGHT 48 | 49 | import bpy 50 | for i, ob in enumerate(bpy.data.objects): 51 | print(i, ob.name, ob.type) 52 | 53 | 54 | # The following snippet adds the letter 'z' in front of the name of the first 55 | # object stored in the current session. The expected result if run in the 56 | # default scene is that "Camera" is renamed to "zCamera" 57 | 58 | import bpy 59 | bpy.data.objects[0].name ='z' + bpy.data.objects[0].name 60 | 61 | 62 | # The following snippet FAILS to prepend ONE "z" in front of each object's name: 63 | # because of how blender's API works, more than 50 "z" letters are added instead 64 | 65 | import bpy 66 | for ob in bpy.data.objects: 67 | ob.name ='z' + ob.name 68 | 69 | 70 | # The following snippet manages to add ONE "z" in front of each object's name 71 | # by converting bpy.data.objects to a python list 72 | 73 | import bpy 74 | for ob in list(bpy.data.objects): 75 | ob.name = 'z' + ob.name 76 | 77 | 78 | # The following lines print each object's name using the collection's search keys 79 | for name in bpy.data.objects.keys(): 80 | print(name) 81 | 82 | # The following lines print each object's name and type using the collection values 83 | 84 | for ob in bpy.data.objects.values(): 85 | print(ob.name, ob.type) 86 | 87 | 88 | # The following lines print each object's name and type using the collection items 89 | 90 | for name, ob in bpy.data.objects.items(): 91 | print(name, ob.type) 92 | -------------------------------------------------------------------------------- /ch2/0400_accessing_blender_data.py: -------------------------------------------------------------------------------- 1 | 2 | # The following lines create a new empty object and link it to the scene 3 | 4 | import bpy 5 | my_empty = bpy.data.objects.new('My Empty', None) 6 | print('New Empty created:', my_empty) 7 | 8 | bpy.data.collections['Collection'].objects.link(my_empty) 9 | 10 | 11 | # The following line removes the empty created in the previous snippet 12 | 13 | bpy.data.objects.remove(my_empty) 14 | 15 | 16 | # The following lines work with blender's default scene and remove 17 | # the object 'Cube' from the scene, but not from bpy.data.objects 18 | 19 | collection = bpy.data.collections['Collection'] 20 | collection.objects.unlink(bpy.data.objects['Cube']) 21 | -------------------------------------------------------------------------------- /ch2/0500_understanding_user_context.py: -------------------------------------------------------------------------------- 1 | 2 | # The following snippet adds a new scene to the current session 3 | # and makes it active. The result is visible in the Viewport's header 4 | 5 | import bpy 6 | new_scene = bpy.data.scenes.new('Another Scene') 7 | bpy.context.window.scene = new_scene 8 | 9 | print('The current scene is', bpy.context.scene.name) 10 | 11 | 12 | # The following snippet adds a new layer to the current session 13 | # and makes it active. The result is visible in the Viewport header 14 | 15 | import bpy 16 | new_layer = bpy.context.scene.view_layers.new('My Layer') 17 | print('New layer created:', new_layer.name) 18 | 19 | bpy.context.window.view_layer = new_layer 20 | print('Current layer:', bpy.context.view_layer.name) 21 | 22 | 23 | # The following snippet can be run in blender's default scene and 24 | # makes 'Camera' the active object in the current scene 25 | 26 | import bpy 27 | view_layer = bpy.context.view_layer 28 | view_layer.objects.active = bpy.data.objects['Camera'] 29 | 30 | 31 | # The following snippet prints the names of selected objects and 32 | # wether they are the active object or not""" 33 | 34 | # The expected result after pressing A in blender's default scene is: 35 | 36 | """ 37 | Cube is active, skipping 38 | Light is selected 39 | Camera is selected 40 | """ 41 | 42 | import bpy 43 | 44 | for ob in bpy.context.selected_objects: 45 | if ob is bpy.context.object: 46 | print(ob.name, 'is active, skipping') 47 | continue 48 | print(ob.name, 'is selected') 49 | 50 | 51 | # The following snippet deselect each selected object 52 | 53 | import bpy 54 | for ob in bpy.context.selected_objects: 55 | ob.select_set(False) 56 | 57 | 58 | # The following snippet creates two view layers, one where mesh objects 59 | # are selected and one where cameras are selected. 60 | 61 | import bpy 62 | m_layer = bpy.context.scene.view_layers.new('Sel_Mesh') 63 | c_layer = bpy.context.scene.view_layers.new('Sel_Cam') 64 | 65 | for ob in bpy.data.objects: 66 | ob.select_set(ob.type == 'MESH', view_layer=m_layer) 67 | ob.select_set(ob.type == 'CAMERA', view_layer=c_layer) 68 | -------------------------------------------------------------------------------- /ch3/addons/object_collector.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "Collector", 4 | "author": "John Doe", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "description": "Create collections for object types", 8 | "category": "Object", 9 | } 10 | 11 | 12 | import bpy 13 | 14 | 15 | class OBJECT_OT_collector_types(bpy.types.Operator): 16 | """Create collections based on objects types""" 17 | bl_idname = "object.pckt_type_collector" 18 | bl_label = "Create Type Collections" 19 | 20 | @classmethod 21 | def poll(cls, context): 22 | return len(context.scene.objects) > 0 23 | 24 | @staticmethod 25 | def get_collection(name): 26 | """Returns the collection named after the given 27 | argument. If it doesn’t exist, a new collection 28 | is created and linked to the scene 29 | 30 | Example: 31 | 32 | >>> OBJECT_OT_collector_types.get_collection("Mesh") 33 | bpy.data.collections['Mesh'] 34 | """ 35 | 36 | try: 37 | return bpy.data.collections[name] 38 | except KeyError: 39 | cl = bpy.data.collections.new(name) 40 | bpy.context.scene.collection.children.link(cl) 41 | return cl 42 | 43 | def execute(self, context): 44 | for ob in context.scene.objects: 45 | cl = self.get_collection(ob.type.title()) 46 | try: 47 | cl.objects.link(ob) 48 | except RuntimeError: 49 | continue 50 | 51 | return {'FINISHED'} 52 | 53 | 54 | def draw_collector_item(self, context): 55 | # Menu functions must accept self and context as argument 56 | # context is left unused in this case 57 | row = self.layout.row() 58 | row.operator(OBJECT_OT_collector_types.bl_idname) 59 | 60 | 61 | def register(): 62 | bpy.utils.register_class(OBJECT_OT_collector_types) 63 | bpy.types.VIEW3D_MT_object_context_menu.append(draw_collector_item) 64 | 65 | 66 | def unregister(): 67 | bpy.utils.unregister_class(OBJECT_OT_collector_types) 68 | bpy.types.VIEW3D_MT_pose_context_menu.remove(draw_collector_item) 69 | -------------------------------------------------------------------------------- /ch3/the_simplest_add_on.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "The Simplest Add-on", 4 | "author": "John Doe", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "description": "A very simple add-on", 8 | "warning": "This is just for Learning", 9 | "category": "Learning", 10 | } 11 | 12 | 13 | def register(): 14 | # this function is called when the add-on is enabled 15 | pass 16 | 17 | def unregister(): 18 | # this function is called when the add-on is disabled 19 | pass 20 | -------------------------------------------------------------------------------- /ch4/addons/object_floor_transform.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "Elevator", 4 | "author": "John Doe", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "description": "Move objects up to a minimum height", 8 | "category": "Object", 9 | } 10 | 11 | 12 | import bpy 13 | from bpy.props import FloatProperty 14 | from bpy.props import BoolProperty 15 | 16 | from copy import copy 17 | 18 | 19 | def ancestors_count(ob): 20 | """Return number of objects up in the hierarchy""" 21 | ancestors = 0 22 | while ob.parent: 23 | ancestors += 1 24 | ob = ob.parent 25 | 26 | return ancestors 27 | 28 | 29 | def get_constraint(ob, constr_type, reuse=True): 30 | """Return first constraint of given type. 31 | If not found, a new one is created""" 32 | if reuse: 33 | for constr in ob.constraints: 34 | if constr.type == constr_type: 35 | return constr 36 | 37 | return ob.constraints.new(constr_type) 38 | 39 | 40 | class OBJECT_OT_elevator(bpy.types.Operator): 41 | """Move Objects up to a certain height""" 42 | bl_idname = "object.pckt_floor_transform" 43 | bl_label = "Elevate Objects" 44 | bl_options = {'REGISTER', 'UNDO'} 45 | 46 | floor: FloatProperty(name="Floor", default=0) 47 | constr: BoolProperty(name="Constraints", default=False) 48 | reuse: BoolProperty(name="Reuse", default=True) 49 | 50 | @classmethod 51 | def poll(cls, context): 52 | return len(bpy.context.selected_objects) > 0 53 | 54 | def execute(self, context): 55 | if self.constr: 56 | for ob in context.selected_objects: 57 | limit = get_constraint(ob, 'LIMIT_LOCATION', self.reuse) 58 | 59 | limit.use_min_z = True 60 | limit.min_z = self.floor 61 | 62 | return {'FINISHED'} 63 | 64 | # affect coordinates directly 65 | # sort parent objects first 66 | selected_objects = copy(context.selected_objects) 67 | selected_objects.sort(key=ancestors_count) 68 | 69 | for ob in selected_objects: 70 | matrix_world = ob.matrix_world 71 | 72 | if matrix_world[2][3] > self.floor: 73 | continue 74 | 75 | matrix_world[2][3] = self.floor 76 | # make sure next object matrix will be updated 77 | context.view_layer.update() 78 | 79 | return {'FINISHED'} 80 | 81 | 82 | def draw_elevator_item(self, context): 83 | # Menu functions must accept self and context as argument 84 | # context is left unused in this case 85 | row = self.layout.row() 86 | row.operator(OBJECT_OT_elevator.bl_idname) 87 | 88 | 89 | def register(): 90 | bpy.utils.register_class(OBJECT_OT_elevator) 91 | bpy.types.VIEW3D_MT_object_context_menu.append(draw_elevator_item) 92 | 93 | 94 | def unregister(): 95 | bpy.utils.unregister_class(OBJECT_OT_elevator) 96 | bpy.types.VIEW3D_MT_object_context_menu.remove(draw_elevator_item) 97 | -------------------------------------------------------------------------------- /ch5/addons/icon_smile_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Scripting-in-Blender/3e0c7afc69c783c9cc6c57abce3b284d1395d044/ch5/addons/icon_smile_64.png -------------------------------------------------------------------------------- /ch5/addons/very_simple_panel.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "A Very Simple Panel", 4 | "author": "John Doe", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "description": "Just show up a panel in the UI", 8 | "category": "Learning", 9 | } 10 | 11 | 12 | import bpy 13 | from bpy.utils import previews 14 | import os 15 | import random 16 | 17 | 18 | # global variable for icon storage 19 | custom_icons = None 20 | 21 | 22 | def load_custom_icons(): 23 | """Load icon from the add-on folder""" 24 | addon_path = os.path.dirname(__file__) 25 | img_file = os.path.join(addon_path, "icon_smile_64.png") 26 | global custom_icons 27 | 28 | custom_icons = previews.new() 29 | custom_icons.load("smile_face",img_file, 'IMAGE') 30 | 31 | 32 | def clear_custom_icons(): 33 | """Clear Icons loaded from file""" 34 | global custom_icons 35 | 36 | bpy.utils.previews.remove(custom_icons) 37 | 38 | 39 | def add_random_location(objects, amount=1, 40 | do_axis=(True, True, True)): 41 | """Add units to the locations of given objects""" 42 | for ob in objects: 43 | for i in range(3): 44 | if do_axis[i]: 45 | loc = ob.location 46 | loc[i] += random.randint(-amount, amount) 47 | 48 | 49 | class TRANSFORM_OT_random_location(bpy.types.Operator): 50 | """Add units to the locations of selected objects""" 51 | bl_idname = "transform.add_random_location" 52 | bl_label = "Add random Location" 53 | 54 | amount: bpy.props.IntProperty(name="Amount", 55 | min=0, # prevent negative values to avoid errors in add_random_location() 56 | default=1) 57 | axis: bpy.props.BoolVectorProperty( 58 | name="Displace Axis", 59 | default=(True, True, True), 60 | subtype='XYZ' 61 | ) 62 | 63 | @classmethod 64 | def poll(cls, context): 65 | return context.selected_objects 66 | 67 | def invoke(self, context, event): 68 | wm = context.window_manager 69 | return wm.invoke_props_dialog(self) 70 | 71 | def execute(self, context): 72 | add_random_location(context.selected_objects, 73 | self.amount, 74 | self.axis) 75 | return {'FINISHED'} 76 | 77 | 78 | class OBJECT_PT_very_simple(bpy.types.Panel): 79 | """Creates a Panel in the object context of the properties editor""" 80 | bl_label = "A Very Simple Panel" 81 | bl_idname = "VERYSIMPLE_PT_layout" 82 | bl_space_type = 'PROPERTIES' 83 | bl_region_type = 'WINDOW' 84 | bl_context = 'object' 85 | 86 | max_objects = 3 87 | 88 | def draw(self, context): 89 | layout = self.layout 90 | layout.label(text="A Very Simple Label", icon='INFO') 91 | layout.label(text="Isn't it great?", icon='QUESTION') 92 | layout.label(text="Smile", icon_value=custom_icons["smile_face"].icon_id) 93 | 94 | col = layout.column() 95 | box = col.box() 96 | split = box.split(factor=0.33) 97 | left_col = split.column() 98 | right_col = split.column() 99 | 100 | for k, v in bl_info.items(): 101 | if not v: 102 | # ignore empty entries 103 | continue 104 | 105 | left_col.label(text=k) 106 | right_col.label(text=str(v)) 107 | 108 | col.label(text="Scene Objects:") 109 | grid = col.grid_flow(columns=2, row_major=True) 110 | for i, ob in enumerate(context.scene.objects): 111 | if i > self.max_objects: 112 | objects_left = len(context.scene.objects) 113 | objects_left -= self.max_objects 114 | grid.label(text=f"... (other {objects_left} objects)") 115 | 116 | break 117 | 118 | # layout item to set entry color 119 | item_layout = grid.column() 120 | 121 | item_layout.enabled = ob.select_get() 122 | item_layout.alert = ob == context.object 123 | item_layout.label(text=ob.name, icon=f'OUTLINER_OB_{ob.type}') 124 | 125 | num_selected = len(context.selected_objects) 126 | if (num_selected > 0): 127 | op_txt = f"Delete {num_selected} object" 128 | if num_selected > 1: 129 | op_txt += "s" # add plural 's' 130 | 131 | props = col.operator(bpy.ops.object.delete.idname(), text=op_txt) 132 | props.confirm = False 133 | else: 134 | to_disable = col.column() 135 | to_disable.enabled = False 136 | to_disable.operator(bpy.ops.object.delete.idname(), 137 | text="Delete Selected") 138 | 139 | col.operator(TRANSFORM_OT_random_location.bl_idname) 140 | 141 | 142 | 143 | def register(): 144 | load_custom_icons() 145 | bpy.utils.register_class(TRANSFORM_OT_random_location) 146 | bpy.utils.register_class(OBJECT_PT_very_simple) 147 | 148 | 149 | def unregister(): 150 | bpy.utils.unregister_class(OBJECT_PT_very_simple) 151 | bpy.utils.unregister_class(TRANSFORM_OT_random_location) 152 | clear_custom_icons() 153 | -------------------------------------------------------------------------------- /ch6/addons/structured_addon/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "A Structured Add-on", 4 | "author": "John Doe", 5 | "version": (1, 0), 6 | "blender": (3, 2, 0), 7 | "description": "Add-on consisting of multiple files", 8 | "category": "Learning", 9 | } 10 | 11 | 12 | from . import operators 13 | from . import img_loader 14 | from . import panel 15 | from . import preferences 16 | from . import _refresh_ 17 | _refresh_.reload_modules() 18 | 19 | 20 | def register(): 21 | preferences.register_classes() 22 | operators.register_classes() 23 | img_loader.register_icons() 24 | panel.register_classes() 25 | 26 | 27 | def unregister(): 28 | panel.unregister_classes() 29 | img_loader.unregister_icons() 30 | operators.unregister_classes() 31 | preferences.unregister_classes() 32 | -------------------------------------------------------------------------------- /ch6/addons/structured_addon/_refresh_.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from importlib import reload 3 | import bpy 4 | 5 | from . import * 6 | 7 | def reload_modules(): 8 | if not bpy.context.preferences.view.show_developer_ui: 9 | return 10 | reload(sys.modules[__name__]) 11 | reload(img_loader) 12 | reload(preferences) 13 | reload(operators) 14 | reload(panel) 15 | -------------------------------------------------------------------------------- /ch6/addons/structured_addon/img_loader.py: -------------------------------------------------------------------------------- 1 | from bpy.utils import previews 2 | import os 3 | 4 | 5 | # global list for storing icon collection 6 | _CUSTOM_ICONS = None 7 | 8 | 9 | def register_icons(): 10 | """Load icon from the add-on folder""" 11 | global _CUSTOM_ICONS 12 | if _CUSTOM_ICONS: 13 | # the collection list is already loaded 14 | return 15 | 16 | collection = previews.new() 17 | img_extensions = ('.png', '.jpg') 18 | 19 | module_path = os.path.dirname(__file__) 20 | picture_path = os.path.join(module_path, 'pictures') 21 | for img_file in os.listdir(picture_path): 22 | img_name, ext = os.path.splitext(img_file) 23 | 24 | if ext.lower() not in img_extensions: 25 | continue 26 | 27 | disk_path = os.path.join(picture_path, img_file) 28 | collection.load(img_name, disk_path, 'IMAGE') 29 | 30 | _CUSTOM_ICONS = collection 31 | 32 | 33 | def unregister_icons(): 34 | """Removes all loaded icons""" 35 | global _CUSTOM_ICONS 36 | if _CUSTOM_ICONS: 37 | previews.remove(_CUSTOM_ICONS) 38 | 39 | _CUSTOM_ICONS = None 40 | 41 | 42 | def get_icons_collection(): 43 | # load icons from disk 44 | register_icons() 45 | 46 | # at this point, we should have icons. A None _CUSTOM_ICONS would cause an error 47 | assert _CUSTOM_ICONS 48 | return _CUSTOM_ICONS 49 | -------------------------------------------------------------------------------- /ch6/addons/structured_addon/objects_panel.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import picture_loader 3 | 4 | 5 | class OBJECT_PT_structured(bpy.types.Panel): 6 | """Creates a Panel in the object context of the properties editor""" 7 | bl_label = "A Modular Panel" 8 | bl_idname = "MODULAR_PT_layout" 9 | bl_space_type = 'PROPERTIES' 10 | bl_region_type = 'WINDOW' 11 | bl_context = 'object' 12 | 13 | 14 | def draw(self, context): 15 | layout = self.layout 16 | icons = picture_loader.get_icons_collection() 17 | 18 | row = layout.row(align=True) 19 | row.label(text="Scene Objects", icon_value=icons['pack_64'].icon_id) 20 | row.label(text="", icon_value=icons["smile_64"].icon_id) 21 | 22 | grid = layout.grid_flow(columns=2, row_major=True) 23 | add_on = context.preferences.addons[__package__] 24 | preferences = add_on.preferences 25 | 26 | for i, ob in enumerate(context.scene.objects): 27 | if i >= preferences.max_objects: 28 | grid.label(text="...") 29 | break 30 | 31 | grid.label(text=ob.name, icon=f'OUTLINER_OB_{ob.type}') 32 | 33 | 34 | 35 | def register_classes(): 36 | bpy.utils.register_class(OBJECT_PT_structured) 37 | 38 | 39 | def unregister_classes(): 40 | bpy.utils.unregister_class(OBJECT_PT_structured) 41 | -------------------------------------------------------------------------------- /ch6/addons/structured_addon/operators.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import random 3 | 4 | 5 | def add_random_location(objects, amount=1, 6 | do_axis=(True, True, True)): 7 | """Add units to the locations of given objects""" 8 | for ob in objects: 9 | for i in range(3): 10 | if do_axis[i]: 11 | loc = ob.location 12 | loc[i] += random.randint(-amount, amount) 13 | 14 | 15 | class TRANSFORM_OT_random_location(bpy.types.Operator): 16 | """Add units to the locations of selected objects""" 17 | bl_idname = "transform.add_random_location" 18 | bl_label = "Add random Location" 19 | amount: bpy.props.IntProperty(name="Amount", 20 | default=1) 21 | axis: bpy.props.BoolVectorProperty( 22 | name="Displace Axis", 23 | default=(True, True, True) 24 | ) 25 | @classmethod 26 | def poll(cls, context): 27 | return context.selected_objects 28 | 29 | def execute(self, context): 30 | add_random_location(context.selected_objects, 31 | self.amount, 32 | self.axis) 33 | return {'FINISHED'} 34 | 35 | 36 | def register_classes(): 37 | bpy.utils.register_class(TRANSFORM_OT_random_location) 38 | def unregister_classes(): 39 | bpy.utils.unregister_class(TRANSFORM_OT_random_location) 40 | -------------------------------------------------------------------------------- /ch6/addons/structured_addon/panel.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from . import img_loader 3 | from . import operators 4 | 5 | 6 | class OBJECT_PT_structured(bpy.types.Panel): 7 | """Creates a Panel in the object context of the properties editor""" 8 | bl_label = "A Modular Panel" 9 | bl_idname = "MODULAR_PT_layout" 10 | bl_space_type = 'PROPERTIES' 11 | bl_region_type = 'WINDOW' 12 | bl_context = 'object' 13 | 14 | 15 | def draw(self, context): 16 | layout = self.layout 17 | icons = img_loader.get_icons_collection() 18 | 19 | row = layout.row(align=True) 20 | row.label(text="Scene Objects", icon_value=icons['pack_64'].icon_id) 21 | row.label(text="", icon_value=icons["smile_64"].icon_id) 22 | 23 | grid = layout.grid_flow(columns=2, row_major=True) 24 | add_on = context.preferences.addons[__package__] 25 | preferences = add_on.preferences 26 | 27 | for i, ob in enumerate(context.scene.objects): 28 | if i >= preferences.max_objects: 29 | grid.label(text="...") 30 | break 31 | 32 | grid.label(text=ob.name, icon=f'OUTLINER_OB_{ob.type}') 33 | 34 | layout.operator(operators.TRANSFORM_OT_random_location.bl_idname) 35 | 36 | 37 | 38 | def register_classes(): 39 | bpy.utils.register_class(OBJECT_PT_structured) 40 | 41 | 42 | def unregister_classes(): 43 | bpy.utils.unregister_class(OBJECT_PT_structured) 44 | -------------------------------------------------------------------------------- /ch6/addons/structured_addon/pictures/pack_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Scripting-in-Blender/3e0c7afc69c783c9cc6c57abce3b284d1395d044/ch6/addons/structured_addon/pictures/pack_64.png -------------------------------------------------------------------------------- /ch6/addons/structured_addon/pictures/smile_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Scripting-in-Blender/3e0c7afc69c783c9cc6c57abce3b284d1395d044/ch6/addons/structured_addon/pictures/smile_64.png -------------------------------------------------------------------------------- /ch6/addons/structured_addon/preferences.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.props import IntProperty 3 | 4 | 5 | class StructuredPreferences(bpy.types.AddonPreferences): 6 | bl_idname = __package__ 7 | 8 | max_objects: IntProperty( 9 | name="Maximum number of displayed objects", 10 | default=3, 11 | ) 12 | 13 | def draw(self, context): 14 | layout = self.layout 15 | layout.label(text="Panel Preferences") 16 | 17 | split = layout.split(factor=0.5) 18 | split.separator() 19 | split.label(text="Max Objects:") 20 | split.prop(self, 'max_objects', text="") 21 | 22 | 23 | def register_classes(): 24 | bpy.utils.register_class(StructuredPreferences) 25 | 26 | 27 | def unregister_classes(): 28 | bpy.utils.unregister_class(StructuredPreferences) 29 | -------------------------------------------------------------------------------- /ch7/addons/action_to_range.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "Action to Range", 4 | "author": "John Packt", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "location": "Timeline > View > Action to Scene Range", 8 | "description": "Transfer Action Duration to Scene Range", 9 | "category": "Learning", 10 | } 11 | 12 | 13 | import bpy 14 | 15 | 16 | class ActionToSceneRange(bpy.types.Operator): 17 | """Set Playback range to current action Start/End""" 18 | bl_idname = "anim.action_to_range" 19 | bl_label = "Action to Scene Range" 20 | bl_description = "Transfer action range to scene range" 21 | bl_options = {'REGISTER', 'UNDO'} 22 | 23 | use_preview: bpy.props.BoolProperty(default=False) 24 | 25 | @classmethod 26 | def poll(cls, context): 27 | obj = context.object 28 | 29 | if not obj: 30 | return False 31 | if not obj.animation_data: 32 | return False 33 | if not obj.animation_data.action: 34 | return False 35 | 36 | return True 37 | 38 | def execute(self, context): 39 | first, last = context.object.animation_data.action.frame_range 40 | 41 | scn = context.scene 42 | 43 | scn.use_preview_range = self.use_preview 44 | if self.use_preview: 45 | scn.frame_preview_start = int(first) 46 | scn.frame_preview_end = int(last) 47 | else: 48 | scn.frame_start = int(first) 49 | scn.frame_end = int(last) 50 | 51 | try: 52 | bpy.ops.action.view_all() 53 | except RuntimeError: 54 | # we are not in the timeline context 55 | for window in context.window_manager.windows: 56 | screen = window.screen 57 | for area in screen.areas: 58 | if area.type != 'DOPESHEET_EDITOR': 59 | continue 60 | for region in area.regions: 61 | if region.type == 'WINDOW': 62 | with context.temp_override(window=window, 63 | area=area, 64 | region=region): 65 | bpy.ops.action.view_all() 66 | break 67 | break 68 | 69 | return {'FINISHED'} 70 | 71 | 72 | def menu_func(self, context): 73 | props = self.layout.operator(ActionToSceneRange.bl_idname, 74 | text=ActionToSceneRange.bl_label + " (preview)") 75 | props.use_preview = True 76 | 77 | props = self.layout.operator(ActionToSceneRange.bl_idname, 78 | text=ActionToSceneRange.bl_label) 79 | props.use_preview = False 80 | 81 | 82 | def register(): 83 | bpy.utils.register_class(ActionToSceneRange) 84 | bpy.types.TIME_MT_view.append(menu_func) 85 | 86 | def unregister(): 87 | bpy.types.TIME_MT_view.remove(menu_func) 88 | bpy.utils.unregister_class(ActionToSceneRange) 89 | -------------------------------------------------------------------------------- /ch7/addons/vert_runner.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "Vert Runner", 4 | "author": "John Packt", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "location": "Object > Animation > Vert Runner", 8 | "description": "Run over vertices of the active object", 9 | "category": "Learning", 10 | } 11 | 12 | 13 | import bpy 14 | from math import asin, pi 15 | 16 | 17 | class VertRunner(bpy.types.Operator): 18 | """Run over the vertices of the active object""" 19 | bl_idname = "object.vert_runner" 20 | bl_label = "Vertex Runner" 21 | bl_description = "Animate along vertices of active object" 22 | bl_options = {'REGISTER', 'UNDO'} 23 | 24 | step: bpy.props.IntProperty(default=12) 25 | loop: bpy.props.BoolProperty(default=True) 26 | 27 | @classmethod 28 | def poll(cls, context): 29 | obj = context.object 30 | 31 | if not obj: 32 | return False 33 | 34 | if not obj.type == 'MESH': 35 | return False 36 | 37 | if not len(context.selected_objects) > 1: 38 | return False 39 | 40 | return True 41 | 42 | def aim_to_point(self, ob, target_co): 43 | direction = target_co - ob.location 44 | direction.normalize() 45 | 46 | arc = asin(direction.y) 47 | if direction.x < 0: 48 | arc = pi - arc 49 | 50 | arc += pi / 2 51 | arcs = (arc, arc + 2*pi, arc - 2*pi) 52 | 53 | diffs = [abs(ob.rotation_euler.z - a) for a in arcs] 54 | shortest = min(diffs) 55 | 56 | res = next(a for i, a in enumerate(arcs) if diffs[i] == shortest) 57 | ob.rotation_euler.z = res 58 | 59 | def execute(self, context): 60 | verts = list(context.object.data.vertices) 61 | 62 | if self.loop: 63 | verts.append(verts[0]) 64 | 65 | for ob in context.selected_objects: 66 | if ob == context.active_object: 67 | continue 68 | 69 | # move to last position to orient towards first vertex 70 | ob.location = context.object.data.vertices[-1].co 71 | 72 | frame = context.scene.frame_current 73 | for vert in verts: 74 | # orient towards destination before moving the object 75 | self.aim_to_point(ob, vert.co) 76 | ob.keyframe_insert('rotation_euler', frame=frame, index=2) 77 | 78 | ob.location = vert.co 79 | ob.keyframe_insert('location', frame=frame) 80 | 81 | frame += self.step 82 | 83 | return {'FINISHED'} 84 | 85 | 86 | def anim_menu_func(self, context): 87 | self.layout.separator() 88 | self.layout.operator(VertRunner.bl_idname, 89 | text=VertRunner.bl_label) 90 | 91 | def register(): 92 | bpy.utils.register_class(VertRunner) 93 | bpy.types.VIEW3D_MT_object_animation.append(anim_menu_func) #TODO: header button 94 | 95 | def unregister(): 96 | bpy.types.VIEW3D_MT_object_animation.remove(anim_menu_func) 97 | bpy.utils.unregister_class(VertRunner) 98 | -------------------------------------------------------------------------------- /ch8/_scenes_/ani_loop.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Python-Scripting-in-Blender/3e0c7afc69c783c9cc6c57abce3b284d1395d044/ch8/_scenes_/ani_loop.blend -------------------------------------------------------------------------------- /ch8/addons/object_shaker.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "Object Shaker", 4 | "author": "John Packt", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "description": "Add Shaky motion to active object", 8 | "category": "Learning", 9 | } 10 | 11 | 12 | import bpy 13 | 14 | 15 | class ObjectShaker(bpy.types.Operator): 16 | """Set Playback range to current action Start/End""" 17 | bl_idname = "object.shaker_animation" 18 | bl_label = "Add Object Shake" 19 | bl_description = "Add Shake Motion to Active Object" 20 | bl_options = {'REGISTER', 'UNDO'} 21 | 22 | duration: bpy.props.FloatProperty(default=1.0, min=0.0) 23 | strenght: bpy.props.FloatProperty(default=1.0, soft_min=0.0, soft_max=1.0) 24 | 25 | @classmethod 26 | def poll(cls, context): 27 | obj = context.object 28 | 29 | if not obj: 30 | return False 31 | 32 | return True 33 | 34 | def get_fcurve(self, context, data_path, index): 35 | action = context.object.animation_data.action 36 | 37 | try: 38 | crv = action.fcurves.new(data_path, index=index) 39 | except RuntimeError: 40 | crv = next((fc for fc in action.fcurves 41 | if fc.data_path == data_path and fc.array_index == index), 42 | None) 43 | 44 | if not crv.keyframe_points: 45 | crv.keyframe_points.insert(frame=context.scene.frame_current, 46 | value=getattr(context.object, data_path)[index]) 47 | 48 | return crv 49 | 50 | def execute(self, context): 51 | # create animation data if there is none 52 | if not context.object.animation_data: 53 | anim_data = context.object.animation_data_create() 54 | else: 55 | anim_data = context.object.animation_data 56 | 57 | # create action if there is none 58 | if not anim_data.action: 59 | action = bpy.data.actions.new('ShakeMotion') 60 | anim_data.action = action 61 | else: 62 | action = anim_data.action 63 | 64 | z_loc_crv = self.get_fcurve(context, 'location', index=2) 65 | y_rot_crv = self.get_fcurve(context, 'rotation_euler', index=1) 66 | x_rot_crv = self.get_fcurve(context, 'rotation_euler', index=0) 67 | 68 | for crv in z_loc_crv, y_rot_crv, x_rot_crv: 69 | noise = crv.modifiers.new('NOISE') 70 | noise.use_restricted_range = True 71 | 72 | # NOTE: duration_frames and current could be moved before the for loop 73 | duration_frames = self.duration * context.scene.render.fps / 2 74 | current = context.scene.frame_current 75 | 76 | noise.frame_start = current - duration_frames 77 | noise.frame_end = current + duration_frames 78 | 79 | noise.strength = self.strenght 80 | 81 | return {'FINISHED'} 82 | 83 | 84 | def menu_func(self, context): 85 | self.layout.separator() 86 | self.layout.operator(ObjectShaker.bl_idname) 87 | 88 | 89 | def register(): 90 | bpy.utils.register_class(ObjectShaker) 91 | bpy.types.VIEW3D_MT_object_context_menu.append(menu_func) 92 | 93 | 94 | def unregister(): 95 | bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func) 96 | bpy.utils.unregister_class(ObjectShaker) 97 | -------------------------------------------------------------------------------- /ch9/addons/pendulum.py: -------------------------------------------------------------------------------- 1 | 2 | bl_info = { 3 | "name": "Object Pendulum", 4 | "author": "John Packt", 5 | "version": (1, 0), 6 | "blender": (3, 00, 0), 7 | "description": "Add swing motion to active object", 8 | "category": "Learning", 9 | } 10 | 11 | 12 | import bpy 13 | 14 | 15 | class ObjectPendulum(bpy.types.Operator): 16 | """Set Playback range to current action Start/End""" 17 | bl_idname = "object.shaker_animation" 18 | bl_label = "Make Pendulum" 19 | bl_description = "Add swinging motion to Active Object" 20 | bl_options = {'REGISTER', 'UNDO'} 21 | 22 | amplitude: bpy.props.FloatProperty(default=0.25, min=0.0) 23 | length: bpy.props.FloatProperty(default=5.0, min=0.0) 24 | 25 | @classmethod 26 | def poll(cls, context): 27 | obj = context.object 28 | 29 | if not obj: 30 | return False 31 | 32 | return True 33 | 34 | def execute(self, context): 35 | ob = context.object 36 | 37 | # create pivot point 38 | pivot = bpy.data.objects.new(f"EMP-{ob.name}_pivot", None) 39 | context.collection.objects.link(pivot) 40 | 41 | # move pivot upwards 42 | pivot.matrix_world = ob.matrix_world 43 | pivot.matrix_world[2][3] += self.length 44 | 45 | # add amplitude attr 46 | pivot['amplitude'] = self.amplitude 47 | 48 | constr = ob.constraints.new('PIVOT') 49 | constr.target = pivot 50 | constr.rotation_range = 'ALWAYS_ACTIVE' 51 | 52 | drv_crv = ob.driver_add('rotation_euler', 0) 53 | driver = drv_crv.driver 54 | driver.type = 'SCRIPTED' 55 | driver.expression = 'sin(frame / fps / sqrt(length/9.8)) * amp * pi' 56 | 57 | # add fps var 58 | fps = driver.variables.new() 59 | fps.name = 'fps' 60 | fps.targets[0].id_type = 'SCENE' 61 | fps.targets[0].id = context.scene 62 | fps.targets[0].data_path = 'render.fps' 63 | 64 | # add length var 65 | len = driver.variables.new() 66 | len.name = 'length' 67 | len.type = 'LOC_DIFF' 68 | len.targets[0].id = pivot 69 | len.targets[1].id = ob 70 | 71 | # add amplitude var 72 | amp = driver.variables.new() 73 | amp.name = 'amp' 74 | amp.targets[0].id_type = 'OBJECT' 75 | amp.targets[0].id = pivot 76 | amp.targets[0].data_path = '["amplitude"]' 77 | 78 | return {'FINISHED'} 79 | 80 | 81 | def menu_func(self, context): 82 | self.layout.separator() 83 | self.layout.operator(ObjectPendulum.bl_idname) 84 | 85 | 86 | def register(): 87 | bpy.utils.register_class(ObjectPendulum) 88 | bpy.types.VIEW3D_MT_object_context_menu.append(menu_func) 89 | 90 | 91 | def unregister(): 92 | bpy.types.VIEW3D_MT_object_context_menu.remove(menu_func) 93 | bpy.utils.unregister_class(ObjectPendulum) 94 | --------------------------------------------------------------------------------