├── .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 |
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 |
--------------------------------------------------------------------------------