302 lines
9.2 KiB
Markdown
302 lines
9.2 KiB
Markdown
# Building Blender extensions in this repository
|
|
|
|
This guide targets Blender 5.x, with Blender 5.2 as the current user target.
|
|
Blender calls installable Python plug-ins **add-on extensions**. Use that format
|
|
instead of the legacy single-file add-on format.
|
|
|
|
## 1. Understand the package boundary
|
|
|
|
Every extension is an independent directory under `blender/`:
|
|
|
|
```text
|
|
blender/
|
|
example_tool/
|
|
blender_manifest.toml
|
|
__init__.py
|
|
operators.py # optional
|
|
properties.py # optional
|
|
ui.py # optional
|
|
wheels/ # optional bundled dependencies
|
|
```
|
|
|
|
The build command turns the contents of `example_tool/` into a ZIP whose root
|
|
contains the manifest and `__init__.py`. The directory containing all extensions
|
|
is not itself a Python package.
|
|
|
|
Start with one `__init__.py` while a tool is small. Split it when operators,
|
|
panels, and data helpers become difficult to navigate. Multi-file extensions
|
|
must use relative imports:
|
|
|
|
```python
|
|
from . import operators, properties, ui
|
|
```
|
|
|
|
Do not import the extension by a hard-coded installed name. Blender namespaces
|
|
extensions by repository, for example `bl_ext.user_default.example_tool`, and
|
|
that prefix changes when the package is installed elsewhere.
|
|
|
|
## 2. Write the manifest
|
|
|
|
A minimal repository-compatible manifest looks like this:
|
|
|
|
```toml
|
|
schema_version = "1.0.0"
|
|
|
|
id = "example_tool"
|
|
version = "0.1.0"
|
|
name = "Example Tool"
|
|
tagline = "Perform one useful Blender workflow"
|
|
maintainer = "Your Name"
|
|
type = "add-on"
|
|
|
|
blender_version_min = "5.0.0"
|
|
|
|
license = [
|
|
"SPDX:GPL-3.0-or-later",
|
|
]
|
|
```
|
|
|
|
Manifest rules worth catching early:
|
|
|
|
- `id` is the stable package identity. Do not change it merely to rename the UI.
|
|
- `version` follows semantic versioning. Bump it when distributing an update.
|
|
- `tagline` is short, has no trailing punctuation, and describes an action.
|
|
- `blender_version_min` is the oldest version actually supported, not simply the
|
|
developer's newest installed version.
|
|
- Omit unused optional fields rather than setting them to empty strings/lists.
|
|
- Use SPDX-prefixed license identifiers.
|
|
|
|
### Permissions
|
|
|
|
Declare capabilities that reach outside normal Blender data:
|
|
|
|
```toml
|
|
[permissions]
|
|
files = "Export processed textures selected by the user"
|
|
network = "Synchronize assets with the configured server"
|
|
```
|
|
|
|
Only declare what the extension uses. Permission explanations must be short and
|
|
must not end in punctuation. Network permission does not override Blender's
|
|
online-access preference; check it before connecting:
|
|
|
|
```python
|
|
if not bpy.app.online_access:
|
|
self.report({"ERROR"}, "Enable Online Access in Blender preferences")
|
|
return {"CANCELLED"}
|
|
```
|
|
|
|
## 3. Implement registration, an operator, and UI
|
|
|
|
This is a small but complete extension entry point:
|
|
|
|
```python
|
|
import bpy
|
|
from bpy.props import BoolProperty, PointerProperty
|
|
from bpy.types import Operator, Panel, PropertyGroup
|
|
|
|
|
|
class EXAMPLE_PG_Settings(PropertyGroup):
|
|
affect_selected: BoolProperty(
|
|
name="Affect Selected",
|
|
default=True,
|
|
)
|
|
|
|
|
|
class EXAMPLE_OT_Run(Operator):
|
|
bl_idname = "object.example_run"
|
|
bl_label = "Run Example"
|
|
bl_description = "Perform the example operation on the active mesh"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return bool(
|
|
context.mode == "OBJECT"
|
|
and context.active_object
|
|
and context.active_object.type == "MESH"
|
|
)
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
if obj is None:
|
|
self.report({"ERROR"}, "Select a mesh object")
|
|
return {"CANCELLED"}
|
|
|
|
# Prefer direct data API changes here. Use context, not bpy.context.
|
|
obj["example_was_run"] = True
|
|
self.report({"INFO"}, f"Processed {obj.name}")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class EXAMPLE_PT_Panel(Panel):
|
|
bl_label = "Example Tool"
|
|
bl_idname = "EXAMPLE_PT_main"
|
|
bl_space_type = "VIEW_3D"
|
|
bl_region_type = "UI"
|
|
bl_category = "Example"
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
settings = context.scene.example_tool
|
|
layout.prop(settings, "affect_selected")
|
|
layout.operator("object.example_run")
|
|
|
|
|
|
CLASSES = (
|
|
EXAMPLE_PG_Settings,
|
|
EXAMPLE_OT_Run,
|
|
EXAMPLE_PT_Panel,
|
|
)
|
|
|
|
|
|
def register():
|
|
for cls in CLASSES:
|
|
bpy.utils.register_class(cls)
|
|
bpy.types.Scene.example_tool = PointerProperty(type=EXAMPLE_PG_Settings)
|
|
|
|
|
|
def unregister():
|
|
del bpy.types.Scene.example_tool
|
|
for cls in reversed(CLASSES):
|
|
bpy.utils.unregister_class(cls)
|
|
```
|
|
|
|
Important details:
|
|
|
|
- Register `PropertyGroup` classes before creating pointer/collection properties
|
|
that reference them.
|
|
- Unregister in exact reverse dependency order.
|
|
- Prefix class names and Blender identifiers to avoid collisions.
|
|
- A panel's `draw()` executes frequently. It should be cheap and should not
|
|
mutate scene data.
|
|
- Use `poll()` both to disable invalid UI actions and to document required context.
|
|
- `bl_options = {"REGISTER", "UNDO"}` is appropriate for scene-data changes,
|
|
but Undo does not reverse files, HTTP calls, or other external side effects.
|
|
|
|
## 4. Design safe Blender operations
|
|
|
|
Blender data often has multiple users. Editing `obj.data` can change every
|
|
object sharing that mesh, and editing a material can change every object using
|
|
it. When the feature should be non-destructive, copy first:
|
|
|
|
```python
|
|
result_object = source.copy()
|
|
result_object.data = source.data.copy()
|
|
source.users_collection[0].objects.link(result_object)
|
|
```
|
|
|
|
When modifiers must be applied without touching the source:
|
|
|
|
```python
|
|
depsgraph = context.evaluated_depsgraph_get()
|
|
evaluated = source.evaluated_get(depsgraph)
|
|
result_mesh = bpy.data.meshes.new_from_object(
|
|
evaluated,
|
|
preserve_all_data_layers=True,
|
|
depsgraph=depsgraph,
|
|
)
|
|
```
|
|
|
|
Stateful operations should follow a transaction-like structure:
|
|
|
|
```python
|
|
active_before = context.view_layer.objects.active
|
|
selected_before = list(context.selected_objects)
|
|
temporary_object = None
|
|
success = False
|
|
|
|
try:
|
|
# Create copies and perform the operation.
|
|
success = True
|
|
finally:
|
|
# Remove temporary data and restore selection/settings.
|
|
if temporary_object is not None:
|
|
bpy.data.objects.remove(temporary_object, do_unlink=True)
|
|
```
|
|
|
|
Track every temporary data-block you create. Removing an object does not
|
|
automatically remove its orphaned mesh, material, or image. Check `users == 0`
|
|
before removing a data-block that might legitimately be shared.
|
|
|
|
Use `bpy.ops` only when Blender exposes no suitable data API. Operators depend
|
|
on context: editor area, mode, active object, selected objects, active material
|
|
node, render engine, or view layer. A headless test is the quickest way to find
|
|
hidden context assumptions.
|
|
|
|
## 5. Store files and settings correctly
|
|
|
|
An installed extension may live in a read-only system repository, and upgrades
|
|
replace its package directory. Never store user data beside `__init__.py`.
|
|
|
|
Use the per-extension storage API:
|
|
|
|
```python
|
|
storage_dir = bpy.utils.extension_path_user(
|
|
__package__,
|
|
path="cache",
|
|
create=True,
|
|
)
|
|
```
|
|
|
|
Use Blender properties for settings that should live in a `.blend` file, and an
|
|
`AddonPreferences` subclass for user preferences that should apply across
|
|
projects. Access preferences through `__package__`, not a literal module name:
|
|
|
|
```python
|
|
preferences = bpy.context.preferences.addons[__package__].preferences
|
|
```
|
|
|
|
Use `bpy.path.abspath()` for Blender paths such as `//textures/output.png`.
|
|
Validate empty paths and file formats before starting an expensive operation.
|
|
|
|
## 6. Bundle dependencies
|
|
|
|
Extensions must be self-contained. For a third-party Python dependency:
|
|
|
|
1. Download/build wheels for every supported OS and architecture.
|
|
2. Put them under the extension, conventionally `wheels/`.
|
|
3. List each relative wheel path in the manifest's `wheels` array.
|
|
4. Build and test the installed ZIP on every advertised platform.
|
|
|
|
Do not import from the developer's system Python, mutate Blender's bundled
|
|
Python, or invoke `pip` during registration. Pure-Python code may be vendored,
|
|
but keep its license and avoid top-level package-name collisions.
|
|
|
|
## 7. Build and install
|
|
|
|
Build all repository extensions:
|
|
|
|
```bash
|
|
./build-all.sh
|
|
```
|
|
|
|
Build, reinstall, and enable them for the current Blender user:
|
|
|
|
```bash
|
|
./install-all.sh
|
|
```
|
|
|
|
Windows Command Prompt:
|
|
|
|
```bat
|
|
install-all.bat
|
|
```
|
|
|
|
Select a non-default Blender executable with `BLENDER_BIN`. Close running
|
|
Blender instances before reinstalling; an open process retains already imported
|
|
Python modules.
|
|
|
|
For the full validation and release sequence, continue with
|
|
[Testing and releasing](testing-and-releasing.md).
|
|
|
|
## Official references
|
|
|
|
- [Creating Blender extensions](https://docs.blender.org/manual/en/dev/advanced/extensions/getting_started.html)
|
|
- [Extension add-ons, namespaces, storage, and online access](https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html)
|
|
- [Bundling Python wheels](https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html)
|
|
- [Extension command-line arguments](https://docs.blender.org/manual/en/dev/advanced/command_line/extension_arguments.html)
|
|
- [Current Blender Python API](https://docs.blender.org/api/current/)
|
|
- [Blender extension add-on guidelines](https://developer.blender.org/docs/handbook/extensions/addon_guidelines/)
|
|
|