Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2026 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Type-check Blender Python examples and templates against generated stubs.
Each file is checked in a separate MYPY process, running in parallel.
NOTE(@ideasman42): we are nowhere near close to having Blender scripts type check without any type warnings.
This is mainly as a way to check:
- The stubs are valid can be loaded into MYPY.
- The stubs are working as expected,
since errors in the stubs *do* point to errors in the RST documentation.
However, it is not as a way to ensure we have zero typing errors,
as there are too many false positives.
"""
import argparse
import multiprocessing
import os
import subprocess
import sys
from pathlib import Path
# Project root derived from this file's location (doc/python_api/).
SOURCE_DIR = Path(__file__).resolve().parents[2]
STUB_DIR = SOURCE_DIR / "doc" / "python_api" / "stubs"
SKIP = {
"doc/python_api/examples/aud.0.py",
"doc/python_api/examples/bpy.types.HydraRenderEngine.py",
"scripts/templates_py/ui_list_generic.py",
}
def check_file(filepath: str) -> tuple[str, str]:
"""Run mypy on a single file, return (filepath, error_output)."""
env = os.environ.copy()
env["MYPYPATH"] = str(STUB_DIR)
result = subprocess.run(
[
sys.executable, "-m", "mypy", filepath,
"--no-error-summary",
"--explicit-package-bases",
],
capture_output=True,
text=True,
env=env,
)
prefix = filepath + ":"
lines = [
line for line in result.stdout.splitlines()
if line.startswith(prefix)
# Mix-in classes that narrow `bl_*` Literal attributes cause diamond
# inheritance conflicts - a `mypy` limitation, not a stub bug.
and "incompatible with definition in base class" not in line
]
return filepath, "\n".join(lines)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"-j", "--jobs", type=int, default=0,
help=(
"Parallel jobs (default 0 uses CPU count; 1 runs synchronously, "
"streaming each mypy invocation's output directly to the terminal)."
),
)
args = parser.parse_args()
os.chdir(SOURCE_DIR)
# `Path.as_posix()` normalizes backslashes for WIN32 so SKIP paths match.
all_files = [
path.as_posix() for pattern in (
"doc/python_api/examples/*.py",
"scripts/templates_py/*.py",
"tests/python/*.py",
"scripts/modules/**/*.py",
"scripts/startup/**/*.py",
"scripts/addons_core/**/*.py",
) for path in Path().glob(pattern)
]
files = sorted(f for f in all_files if f not in SKIP)
jobs = args.jobs
if jobs <= 0:
jobs = multiprocessing.cpu_count()
errors = 0
if jobs == 1:
# Synchronous: print each file's result as soon as it's ready.
for filepath in files:
_, output = check_file(filepath)
if output:
errors += 1
print(output)
print()
else:
results: dict[str, str] = {}
with multiprocessing.Pool(jobs) as pool:
for filepath, output in pool.imap_unordered(check_file, files):
results[filepath] = output
# Print results in file order.
for filepath in files:
output = results[filepath]
if output:
errors += 1
print(output)
print()
print("Checked {:d} files, {:d} with errors.".format(len(files), errors))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,185 @@
# SPDX-FileCopyrightText: 2024 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import os
import time
def has_module(module_name):
found = False
try:
__import__(module_name)
found = True
except ModuleNotFoundError as ex:
if ex.name != module_name:
raise ex
return found
# These are substituted when this file is copied to the build directory.
BLENDER_VERSION_STRING = "${BLENDER_VERSION_STRING}"
BLENDER_VERSION_DOTS = "${BLENDER_VERSION_DOTS}"
BLENDER_REVISION = "${BLENDER_REVISION}"
BLENDER_REVISION_TIMESTAMP = "${BLENDER_REVISION_TIMESTAMP}"
BLENDER_VERSION_DATE = time.strftime(
"%d/%m/%Y",
time.localtime(int(BLENDER_REVISION_TIMESTAMP) if BLENDER_REVISION_TIMESTAMP != "0" else None),
)
if BLENDER_REVISION != "Unknown":
# SHA1 GIT hash.
BLENDER_VERSION_HASH = BLENDER_REVISION
BLENDER_VERSION_HASH_HTML_LINK = (
"<a href=https://projects.blender.org/blender/blender/commit/{:s}>{:s}</a>".format(
BLENDER_VERSION_HASH, BLENDER_VERSION_HASH,
)
)
else:
# Fallback: Should not be used.
BLENDER_VERSION_HASH = "Hash Unknown"
BLENDER_VERSION_HASH_HTML_LINK = BLENDER_VERSION_HASH
extensions = []
# Downloading can be slow and get in the way of development,
# support "offline" builds.
if not os.environ.get("BLENDER_DOC_OFFLINE", "").strip("0"):
extensions.append("sphinx.ext.intersphinx")
intersphinx_mapping = {"blender_manual": ("https://docs.blender.org/manual/en/dev/", None)}
# Provides copy button next to code-blocks (nice to have but not essential).
if has_module("sphinx_copybutton"):
extensions.append("sphinx_copybutton")
# Exclude line numbers, prompts, and console text.
copybutton_exclude = ".linenos, .gp, .go"
project = "Blender {:s} Python API".format(BLENDER_VERSION_STRING)
root_doc = "index"
copyright = "Blender Authors"
version = BLENDER_VERSION_DOTS
release = BLENDER_VERSION_DOTS
# Set this as the default is a super-set of Python3.
highlight_language = "python3"
# No need to detect encoding.
highlight_options = {"default": {"encoding": "utf-8"}}
# Quiet file not in table-of-contents warnings.
exclude_patterns = [
"include__bmesh.rst",
]
html_title = "Blender Python API"
# The fallback to a built-in theme when `furo` is not found.
html_theme = "default"
if has_module("furo"):
html_theme = "furo"
html_theme_options = {
"light_css_variables": {
"color-brand-primary": "#265787",
"color-brand-content": "#265787",
},
}
html_sidebars = {
"**": [
"sidebar/brand.html",
"sidebar/search.html",
"sidebar/scroll-start.html",
"sidebar/navigation.html",
"sidebar/scroll-end.html",
"sidebar/variant-selector.html",
]
}
# Not helpful since the source is generated, adds to upload size.
html_copy_source = False
html_show_sphinx = False
html_baseurl = "https://docs.blender.org/api/current/"
html_use_opensearch = "https://docs.blender.org/api/current"
html_show_search_summary = True
html_split_index = True
html_static_path = ["static"]
templates_path = ["templates"]
html_context = {
"commit": "{:s} - {:s}".format(BLENDER_VERSION_HASH_HTML_LINK, BLENDER_VERSION_DATE),
}
html_extra_path = ["static"]
html_favicon = "static/favicon.png"
html_logo = "static/blender_logo.svg"
# Disable default `last_updated` value, since this is the date of doc generation, not the one of the source commit.
html_last_updated_fmt = None
if html_theme == "furo":
html_css_files = ["css/theme_overrides.css", "css/version_switch.css"]
html_js_files = ["js/version_switch.js"]
# Needed for latex, PDF generation.
latex_elements = {
"papersize": "a4paper",
}
latex_documents = [
("contents", "contents.tex", "Blender Index", "Blender Foundation", "manual"),
]
# Workaround for useless links leading to compile errors
# See https://github.com/sphinx-doc/sphinx/issues/3866
from sphinx.domains.python import PythonDomain
class PatchedPythonDomain(PythonDomain):
def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode):
if "refspecific" in node:
del node["refspecific"]
return super(PatchedPythonDomain, self).resolve_xref(
env, fromdocname, builder, typ, target, node, contnode)
def register_details_directive(app):
"""
Register a `.. details:: Title` directive.
Wraps content in an HTML ``<details>`` widget so verbose-but-uninteresting
sections (e.g. dunder methods) are foldable. Implemented via ``nodes.raw``
around the parsed content - this avoids needing a custom node class
(Sphinx pickles the doctree for parallel/incremental builds and cannot
reach classes defined in ``conf.py``, which is exec-loaded). Non-HTML
builders ignore ``raw`` nodes and render the content inline.
"""
from html import escape
from docutils import nodes
from docutils.parsers.rst import Directive
class DetailsDirective(Directive):
required_arguments = 0
optional_arguments = 1
final_argument_whitespace = True
has_content = True
def run(self):
summary = self.arguments[0] if self.arguments else "Details"
container = nodes.Element()
self.state.nested_parse(self.content, self.content_offset, container)
children = list(container.children)
container.children = []
for child in children:
child.parent = None
open_tag = nodes.raw(
"",
"<details><summary>{:s}</summary>".format(escape(summary)),
format="html",
)
close_tag = nodes.raw("", "</details>", format="html")
return [open_tag, *children, close_tag]
app.add_directive("details", DetailsDirective)
def setup(app):
app.add_domain(PatchedPythonDomain, override=True)
register_details_directive(app)

View File

@@ -0,0 +1,22 @@
"""
Basic Sound Playback
++++++++++++++++++++
This script shows how to use the classes: :class:`Device`, :class:`Sound` and
:class:`Handle`.
"""
import aud
device = aud.Device()
# Load sound file (it can be a video file with audio).
sound = aud.Sound('music.ogg')
# Play the audio, this return a handle to control play/pause.
handle = device.play(sound)
# If the audio is not too big and will be used often you can buffer it.
sound_buffered = aud.Sound.cache(sound)
handle_buffered = device.play(sound_buffered)
# Stop the sounds (otherwise they play until their ends).
handle.stop()
handle_buffered.stop()

View File

@@ -0,0 +1,45 @@
"""
Hello World Text Example
++++++++++++++++++++++++
Example of using the blf module. For this module to work we
need to use the GPU module :mod:`gpu` as well.
"""
# Import stand alone modules.
import blf
import bpy
font_info = {
"font_id": 0,
"handler": None,
}
def init():
"""init function - runs once"""
import os
# Create a new font object, use external TTF file.
font_path = bpy.path.abspath('//Zeyada.ttf')
# Store the font index - to use later.
if os.path.exists(font_path):
font_info["font_id"] = blf.load(font_path)
else:
# Default font.
font_info["font_id"] = 0
# Set the font drawing routine to run every frame.
font_info["handler"] = bpy.types.SpaceView3D.draw_handler_add(
draw_callback_px, (None, None), 'WINDOW', 'POST_PIXEL')
def draw_callback_px(self, context):
"""Draw on the viewports"""
# BLF drawing routine.
font_id = font_info["font_id"]
blf.position(font_id, 2, 80, 0)
blf.size(font_id, 50.0)
blf.draw(font_id, "Hello World")
if __name__ == '__main__':
init()

View File

@@ -0,0 +1,29 @@
"""
Drawing Text to an Image
++++++++++++++++++++++++
Example showing how text can be drawn into an image.
This can be done by binding an image buffer (:mod:`imbuf`) to the font's ID.
"""
import blf
import imbuf
image_size = 512, 512
font_size = 20
ibuf = imbuf.new(image_size)
font_id = blf.load("/path/to/font.ttf")
blf.color(font_id, 1.0, 1.0, 1.0, 1.0)
blf.size(font_id, font_size)
blf.position(font_id, 0, image_size[1] - font_size, 0)
blf.enable(font_id, blf.WORD_WRAP)
blf.word_wrap(font_id, image_size[0])
with blf.bind_imbuf(font_id, ibuf, display_name="sRGB"):
blf.draw_buffer(font_id, "Lots of wrapped text. " * 50)
imbuf.write(ibuf, filepath="/path/to/image.png")

View File

@@ -0,0 +1,107 @@
# This script uses bmesh operators to make 2 links of a chain.
import bpy
import bmesh
import math
import mathutils
# Make a new BMesh
bm = bmesh.new()
# Add a circle XXX, should return all geometry created, not just verts.
bmesh.ops.create_circle(
bm,
cap_ends=False,
radius=0.2,
segments=8)
# Spin and deal with geometry on side 'a'
edges_start_a = bm.edges[:]
geom_start_a = bm.verts[:] + edges_start_a
ret = bmesh.ops.spin(
bm,
geom=geom_start_a,
angle=math.radians(180.0),
steps=8,
axis=(1.0, 0.0, 0.0),
cent=(0.0, 1.0, 0.0))
edges_end_a = [ele for ele in ret["geom_last"]
if isinstance(ele, bmesh.types.BMEdge)]
del ret
# Extrude and create geometry on side 'b'
ret = bmesh.ops.extrude_edge_only(
bm,
edges=edges_start_a)
geom_extrude_mid = ret["geom"]
del ret
# Collect the edges to spin XXX, 'extrude_edge_only' could return this.
verts_extrude_b = [ele for ele in geom_extrude_mid
if isinstance(ele, bmesh.types.BMVert)]
edges_extrude_b = [ele for ele in geom_extrude_mid
if isinstance(ele, bmesh.types.BMEdge) and ele.is_boundary]
bmesh.ops.translate(
bm,
verts=verts_extrude_b,
vec=(0.0, 0.0, 1.0))
# Create the circle on side 'b'
ret = bmesh.ops.spin(
bm,
geom=verts_extrude_b + edges_extrude_b,
angle=-math.radians(180.0),
steps=8,
axis=(1.0, 0.0, 0.0),
cent=(0.0, 1.0, 1.0))
edges_end_b = [ele for ele in ret["geom_last"]
if isinstance(ele, bmesh.types.BMEdge)]
del ret
# Bridge the resulting edge loops of both spins 'a & b'
bmesh.ops.bridge_loops(
bm,
edges=edges_end_a + edges_end_b)
# Now we have made a links of the chain, make a copy and rotate it
# (so this looks something like a chain)
ret = bmesh.ops.duplicate(
bm,
geom=bm.verts[:] + bm.edges[:] + bm.faces[:])
geom_dupe = ret["geom"]
verts_dupe = [ele for ele in geom_dupe if isinstance(ele, bmesh.types.BMVert)]
del ret
# position the new link
bmesh.ops.translate(
bm,
verts=verts_dupe,
vec=(0.0, 0.0, 2.0))
bmesh.ops.rotate(
bm,
verts=verts_dupe,
cent=(0.0, 1.0, 0.0),
matrix=mathutils.Matrix.Rotation(math.radians(90.0), 3, 'Z'))
# Done with creating the mesh, simply link it into the scene so we can see it
# Finish up, write the bmesh into a new mesh
me = bpy.data.meshes.new("Mesh")
bm.to_mesh(me)
bm.free()
# Add the mesh to the scene
obj = bpy.data.objects.new("Object", me)
bpy.context.collection.objects.link(obj)
# Select and make active
bpy.context.view_layer.objects.active = obj
obj.select_set(True)

View File

@@ -0,0 +1,39 @@
"""
File Loading & Order of Initialization
Since drivers may be evaluated immediately after loading a blend-file it is necessary
to ensure the driver name-space is initialized beforehand.
This can be done by registering text data-blocks to execute on startup,
which executes the scripts before drivers are evaluated.
See *Text -> Register* from Blender's text editor.
.. hint::
You may prefer to use external files instead of Blender's text-blocks.
This can be done using a text-block which executes an external file.
This example runs ``driver_namespace.py`` located in the same directory as the text-blocks blend-file:
.. code-block::
import os
import bpy
blend_dir = os.path.normpath(os.path.join(__file__, "..", ".."))
bpy.utils.execfile(os.path.join(blend_dir, "driver_namespace.py"))
Using ``__file__`` ensures the text resolves to the expected path even when library-linked from another file.
Other methods of populating the drivers name-space can be made to work but tend to be error prone:
Using The ``--python`` command line argument to populate name-space often fails to achieve the desired goal
because the initial evaluation will lookup a function that doesn't exist yet,
marking the driver as invalid - preventing further evaluation.
Populating the driver name-space before the blend-file loads also doesn't work
since opening a file clears the name-space.
It is possible to run a script via the ``--python`` command line argument, before the blend file.
This can register a load-post handler (:mod:`bpy.app.handlers.load_post`) that initializes the name-space.
While this works for background tasks it has the downside that opening the file from the file selector
won't setup the name-space.
"""

View File

@@ -0,0 +1,15 @@
"""
Basic Handler Example
+++++++++++++++++++++
This script shows the most simple example of adding a handler.
"""
import bpy
def my_handler(scene):
print("Frame Change", scene.frame_current)
bpy.app.handlers.frame_change_pre.append(my_handler)

View File

@@ -0,0 +1,21 @@
"""
Persistent Handler Example
++++++++++++++++++++++++++
By default handlers are freed when loading new files, in some cases you may
want the handler stay running across multiple files (when the handler is
part of an add-on for example).
For this the :data:`bpy.app.handlers.persistent` decorator needs to be used.
"""
import bpy
from bpy.app.handlers import persistent
@persistent
def load_handler(dummy):
print("Load Handler:", bpy.data.filepath)
bpy.app.handlers.load_post.append(load_handler)

View File

@@ -0,0 +1,24 @@
"""
Note on Altering Data
+++++++++++++++++++++
Altering data from handlers should be done carefully. While rendering the
``frame_change_pre`` and ``frame_change_post`` handlers are called from one
thread and the viewport updates from a different thread. If the handler changes
data that is accessed by the viewport, this can cause a crash of Blender. In
such cases, lock the interface (Render → Lock Interface or
:data:`bpy.types.RenderSettings.use_lock_interface`) before starting a render.
Below is an example of a mesh that is altered from a handler:
"""
def frame_change_pre(scene):
# A triangle that shifts in the z direction.
zshift = scene.frame_current * 0.1
vertices = [(-1, -1, zshift), (1, -1, zshift), (0, 1, zshift)]
triangles = [(0, 1, 2)]
object = bpy.data.objects["The Object"]
object.data.clear_geometry()
object.data.from_pydata(vertices, [], triangles)

View File

@@ -0,0 +1,12 @@
"""
Run a Function in x Seconds
---------------------------
"""
import bpy
def in_5_seconds():
print("Hello World")
bpy.app.timers.register(in_5_seconds, first_interval=5)

View File

@@ -0,0 +1,13 @@
"""
Run a Function every x Seconds
------------------------------
"""
import bpy
def every_2_seconds():
print("Hello World")
return 2.0
bpy.app.timers.register(every_2_seconds)

View File

@@ -0,0 +1,19 @@
"""
Run a Function n times every x seconds
--------------------------------------
"""
import bpy
counter = 0
def run_10_times():
global counter
counter += 1
print(counter)
if counter == 10:
return None
return 0.1
bpy.app.timers.register(run_10_times)

View File

@@ -0,0 +1,14 @@
"""
Assign parameters to functions
------------------------------
"""
import bpy
import functools
def print_message(message):
print("Message:", message)
bpy.app.timers.register(functools.partial(print_message, "Hello"), first_interval=2.0)
bpy.app.timers.register(functools.partial(print_message, "World"), first_interval=3.0)

View File

@@ -0,0 +1,93 @@
"""
Introduction
------------
.. warning::
Most of this object should only be useful if you actually manipulate i18n stuff from Python.
If you are a regular add-on, you should only bother about :const:`contexts` member,
and the :func:`register`/:func:`unregister` functions! The :func:`pgettext` family of functions
should only be used in rare, specific cases (like e.g. complex "composited" UI strings...).
To add translations to your Python script, you must define a dictionary formatted like that:
``{locale: {msg_key: msg_translation, ...}, ...}`` where:
- locale is either a lang ISO code (e.g. ``fr``), a lang+country code (e.g. ``pt_BR``),
a lang+variant code (e.g. ``sr@latin``), or a full code (e.g. ``uz_UZ@cyrilic``).
- msg_key is a tuple (context, org message) - use, as much as possible, the predefined :const:`contexts`.
- msg_translation is the translated message in given language!
Then, call ``bpy.app.translations.register(__name__, your_dict)`` in your ``register()`` function, and
``bpy.app.translations.unregister(__name__)`` in your ``unregister()`` one.
The ``Manage UI translations`` add-on has several functions to help you collect strings to translate, and
generate the needed Python code (the translation dictionary), as well as optional intermediary po files
if you want some... See
`How to Translate Blender <https://developer.blender.org/docs/handbook/translating/translator_guide/>`_ and
`Using i18n in Blender Code <https://developer.blender.org/docs/handbook/translating/developer_guide/>`_
for more info.
Module References
-----------------
"""
import bpy
# This block can be automatically generated by UI translations addon, which also handles conversion with PO format.
# See also https://developer.blender.org/docs/handbook/translating/translator_guide/#translating-non-official-add-ons
# It can (should) also be put in a different, specific py file.
# ##### BEGIN AUTOGENERATED I18N SECTION #####
# NOTE: You can safely move around this auto-generated block (with the begin/end markers!),
# and edit the translations by hand.
# Just carefully respect the format of the tuple!
# Tuple of tuples ((msgctxt, msgid), (sources, gen_comments), (lang, translation, (is_fuzzy, comments)), ...)
translations_tuple = (
(("*", ""),
((), ()),
("fr_FR", "Project-Id-Version: Copy Settings 0.1.5 (r0)\nReport-Msgid-Bugs-To: \nPOT-Creation-Date: 2013-04-18 15:27:45.563524\nPO-Revision-Date: 2013-04-18 15:38+0100\nLast-Translator: Bastien Montagne <montagne29@wanadoo.fr>\nLanguage-Team: LANGUAGE <LL@li.org>\nLanguage: __POT__\nMIME-Version: 1.0\nContent-Type: text/plain; charset=UTF-8\nContent-Transfer-Encoding: 8bit\n",
(False,
("Blender's translation file (po format).",
"Copyright (C) 2013 The Blender Foundation.",
"This file is distributed under the same license as the Blender package.",
"FIRST AUTHOR <EMAIL@ADDRESS>, YEAR."))),
),
(("Operator", "Render: Copy Settings"),
(("bpy.types.SCENE_OT_render_copy_settings",),
()),
("fr_FR", "Rendu: copier réglages",
(False, ())),
),
(("*", "Copy render settings from current scene to others"),
(("bpy.types.SCENE_OT_render_copy_settings",),
()),
("fr_FR", "Copier les réglages de rendu depuis la scène courante vers dautres",
(False, ())),
),
# ... etc, all messages from your addon.
)
translations_dict = {}
for msg in translations_tuple:
key = msg[0]
for lang, trans, (is_fuzzy, comments) in msg[2:]:
if trans and not is_fuzzy:
translations_dict.setdefault(lang, {})[key] = trans
# ##### END AUTOGENERATED I18N SECTION #####
# Define remaining addon (operators, UI...) here.
def register():
# Usual operator/UI/etc. registration...
bpy.app.translations.register(__name__, translations_dict)
def unregister():
bpy.app.translations.unregister(__name__)
# Usual operator/UI/etc. unregistration...

View File

@@ -0,0 +1,16 @@
"""
Get the property associated with a hovered button.
Returns a tuple of the data-block, data path to the property, and array index.
.. note::
When the property doesn't have an associated :class:`bpy.types.ID` non-ID data may be returned.
This may occur when accessing windowing data, for example, operator properties.
"""
import bpy
# Example inserting keyframe for the hovered property.
active_property = bpy.context.property
if active_property:
datablock, data_path, index = active_property
datablock.keyframe_insert(data_path=data_path, index=index, frame=1)

View File

@@ -0,0 +1,24 @@
import bpy
# Print all objects.
for obj in bpy.data.objects:
print(obj.name)
# Print all scene names in a list.
print(bpy.data.scenes.keys())
# Remove mesh Cube.
if "Cube" in bpy.data.meshes:
mesh = bpy.data.meshes["Cube"]
print("removing mesh", mesh)
bpy.data.meshes.remove(mesh)
# Write images into a file next to the blend.
import os
with open(os.path.splitext(bpy.data.filepath)[0] + ".txt", 'w') as fs:
for image in bpy.data.images:
fs.write("{:s} {:d} x {:d}\n".format(image.filepath, image.size[0], image.size[1]))

View File

@@ -0,0 +1,53 @@
"""
The message bus system can be used to receive notifications when properties of
Blender data-blocks are changed via the data API.
Limitations
-----------
The message bus system is triggered by updates via the RNA system. This means
that the following updates will result in a notification on the message bus:
- Changes via the Python API, for example ``some_object.location.x += 3``.
- Changes via the sliders, fields, and buttons in the user interface.
The following updates do **not** trigger message bus notifications:
- Moving objects in the 3D Viewport.
- Changes performed by the animation system.
Changes done from ``msgbus`` callbacks are not included in related undo steps,
so users can easily skip their effects by using Undo followed by Redo.
Unlike properties ``update`` callbacks, message bus update callbacks are postponed
until all operators have finished executing.
Additionally, for each property the callback is only triggered once per update cycle,
even if the property was changed multiple times during that period.
Example Use
-----------
Below is an example of subscription to changes in the active object's location.
"""
import bpy
# Any Python object can act as the subscription's owner.
owner = object()
subscribe_to = bpy.context.object.location
def msgbus_callback(*args):
# This will print:
# Something changed! (1, 2, 3)
print("Something changed!", args)
bpy.msgbus.subscribe_rna(
key=subscribe_to,
owner=owner,
args=(1, 2, 3),
notify=msgbus_callback,
)

View File

@@ -0,0 +1,8 @@
"""
Some properties are converted to Python objects when you retrieve them. This
needs to be avoided in order to create the subscription, by using
``datablock.path_resolve("property_name", False)``:
"""
import bpy
subscribe_to = bpy.context.object.path_resolve("name", False)

View File

@@ -0,0 +1,7 @@
"""
It is also possible to create subscriptions on a property of all instances of a
certain type:
"""
import bpy
subscribe_to = (bpy.types.Object, "location")

View File

@@ -0,0 +1,56 @@
"""
Calling Operators
-----------------
Provides Python access to calling operators, this includes operators written in
C++, Python or macros.
Only keyword arguments can be used to pass operator properties.
Operators don't have return values as you might expect,
instead they return a set() which is made up of:
``{'RUNNING_MODAL', 'CANCELLED', 'FINISHED', 'PASS_THROUGH'}``.
Common return values are ``{'FINISHED'}`` and ``{'CANCELLED'}``, the latter
meaning that the operator execution was aborted without making any changes or
saving an undo history entry.
If operator was cancelled but there wasn't any reports from it with ``{'ERROR'}`` type,
it will just return ``{'CANCELLED'}`` without raising any exceptions.
However, if there are error reports, a ``RuntimeError`` will be raised
after the operator finishes execution, including all error report messages,
regardless of the return status (even if it was ``{'FINISHED'}``).
Calling an operator in the wrong context will raise a ``RuntimeError``,
there is a poll() method to avoid this problem.
Note that the operator ID (bl_idname) in this example is ``mesh.subdivide``,
``bpy.ops`` is just the access path for Python.
Keywords and Positional Arguments
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
For calling operators keywords are used for operator properties and
positional arguments are used to define how the operator is called.
There are 2 optional positional arguments (documented in detail below).
.. code-block:: python
bpy.ops.test.operator(execution_context, undo)
- execution_context - ``str`` (enum).
- undo - ``bool`` type.
Each of these arguments is optional, but must be given in the order above.
"""
import bpy
# Calling an operator.
bpy.ops.mesh.subdivide(number_cuts=3, smoothness=0.5)
# Check poll() to avoid exception.
if bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='EDIT')

View File

@@ -0,0 +1,31 @@
"""
Overriding Context
------------------
It is possible to override context members that the operator sees, so that they
act on specified rather than the selected or active data, or to execute an
operator in the different part of the user interface.
The context overrides are passed in as keyword arguments,
with keywords matching the context member names in ``bpy.context``.
For example to override ``bpy.context.active_object``,
you would pass ``active_object=object`` to :class:`bpy.types.Context.temp_override`.
.. note::
You will nearly always want to use a copy of the actual current context as basis
(otherwise, you'll have to find and gather all needed data yourself).
.. note::
Context members are names which Blender uses for data access,
overrides do not extend to overriding methods or any Python specific functionality.
"""
# Remove all objects in scene rather than the selected ones.
import bpy
from bpy import context
context_override = context.copy()
context_override["selected_objects"] = list(context.scene.objects)
with context.temp_override(**context_override):
bpy.ops.object.delete()

View File

@@ -0,0 +1,34 @@
"""
.. _operator-execution_context:
Execution Context
-----------------
When calling an operator you may want to pass the execution context.
This determines the context that is given for the operator to run in, and whether
invoke() is called or only execute().
``EXEC_DEFAULT`` is used by default, running only the ``execute()`` method, but you may
want the operator to take user interaction with ``INVOKE_DEFAULT`` which will also
call invoke() if existing.
The execution context is one of:
- ``INVOKE_DEFAULT``
- ``INVOKE_REGION_WIN``
- ``INVOKE_REGION_CHANNELS``
- ``INVOKE_REGION_PREVIEW``
- ``INVOKE_AREA``
- ``INVOKE_SCREEN``
- ``EXEC_DEFAULT``
- ``EXEC_REGION_WIN``
- ``EXEC_REGION_CHANNELS``
- ``EXEC_REGION_PREVIEW``
- ``EXEC_AREA``
- ``EXEC_SCREEN``
"""
# Collection add popup.
import bpy
bpy.ops.object.collection_instance_add('INVOKE_DEFAULT')

View File

@@ -0,0 +1,16 @@
"""
It is also possible to run an operator in a particular part of the user
interface. For this we need to pass the window, area and sometimes a region.
"""
# Maximize 3d view in all windows.
import bpy
from bpy import context
for window in context.window_manager.windows:
screen = window.screen
for area in screen.areas:
if area.type == 'VIEW_3D':
with context.temp_override(window=window, area=area):
bpy.ops.screen.screen_full_area()
break

View File

@@ -0,0 +1,27 @@
"""
Assigning to Existing Classes
+++++++++++++++++++++++++++++
Custom properties can be added to any subclass of an :class:`ID`,
:class:`Bone` and :class:`PoseBone`.
These properties can be animated, accessed by the user interface and Python
like Blender's existing properties.
.. warning::
Access to these properties might happen in threaded context, on a per-data-block level.
This has to be carefully considered when using accessors or update callbacks.
Typically, these callbacks should not affect any other data that the one owned by their data-block.
When accessing external non-Blender data, thread safety mechanisms should be considered.
"""
import bpy
# Assign a custom property to an existing type.
bpy.types.Material.custom_float = bpy.props.FloatProperty(name="Test Property")
# Test the property is there.
bpy.data.materials[0].custom_float = 5.0

View File

@@ -0,0 +1,64 @@
"""
Operator Example
++++++++++++++++
A common use of custom properties is for Python based :class:`Operator`
classes. Test this code by running it in the text editor, or by clicking the
button in the 3D Viewport's Tools panel. The latter will show the properties
in the Redo panel and allow you to change them.
"""
import bpy
class OBJECT_OT_property_example(bpy.types.Operator):
bl_idname = "object.property_example"
bl_label = "Property Example"
bl_options = {'REGISTER', 'UNDO'}
my_float: bpy.props.FloatProperty(name="Some Floating Point")
my_bool: bpy.props.BoolProperty(name="Toggle Option")
my_string: bpy.props.StringProperty(name="String Value")
def execute(self, context):
self.report(
{'INFO'}, "F: {:.2f} B: {!s} S: {!r}".format(
self.my_float, self.my_bool, self.my_string,
)
)
print('My float:', self.my_float)
print('My bool:', self.my_bool)
print('My string:', self.my_string)
return {'FINISHED'}
class OBJECT_PT_property_example(bpy.types.Panel):
bl_idname = "object_PT_property_example"
bl_label = "Property Example"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Tool"
def draw(self, context):
# You can set the property values that should be used when the user
# presses the button in the UI.
props = self.layout.operator('object.property_example')
props.my_bool = True
props.my_string = "Shouldn't that be 47?"
# You can set properties dynamically:
if context.object:
props.my_float = context.object.location.x
else:
props.my_float = 327
bpy.utils.register_class(OBJECT_OT_property_example)
bpy.utils.register_class(OBJECT_PT_property_example)
# Demo call. Be sure to also test in the 3D Viewport.
bpy.ops.object.property_example(
my_float=47,
my_bool=True,
my_string="Shouldn't that be 327?",
)

View File

@@ -0,0 +1,27 @@
"""
PropertyGroup Example
+++++++++++++++++++++
PropertyGroups can be used for collecting custom settings into one value
to avoid many individual settings mixed in together.
"""
import bpy
class MaterialSettings(bpy.types.PropertyGroup):
my_int: bpy.props.IntProperty()
my_float: bpy.props.FloatProperty()
my_string: bpy.props.StringProperty()
bpy.utils.register_class(MaterialSettings)
bpy.types.Material.my_settings = bpy.props.PointerProperty(type=MaterialSettings)
# Test the new settings work.
material = bpy.data.materials[0]
material.my_settings.my_int = 5
material.my_settings.my_float = 3.0
material.my_settings.my_string = "Foo"

View File

@@ -0,0 +1,34 @@
"""
Collection Example
++++++++++++++++++
Custom properties can be added to any subclass of an :class:`ID`,
:class:`Bone` and :class:`PoseBone`.
"""
import bpy
# Assign a collection.
class SceneSettingItem(bpy.types.PropertyGroup):
name: bpy.props.StringProperty(name="Test Property", default="Unknown")
value: bpy.props.IntProperty(name="Test Property", default=22)
bpy.utils.register_class(SceneSettingItem)
bpy.types.Scene.my_settings = bpy.props.CollectionProperty(type=SceneSettingItem)
# Assume an armature object selected.
print("Adding 2 values!")
my_item = bpy.context.scene.my_settings.add()
my_item.name = "Spam"
my_item.value = 1000
my_item = bpy.context.scene.my_settings.add()
my_item.name = "Eggs"
my_item.value = 30
for my_item in bpy.context.scene.my_settings:
print(my_item.name, my_item.value)

View File

@@ -0,0 +1,34 @@
"""
Update Example
++++++++++++++
It can be useful to perform an action when a property is changed and can be
used to update other properties or synchronize with external data.
All properties define update functions except for CollectionProperty.
.. warning::
Remember that these callbacks may be executed in threaded context.
.. warning::
If the property belongs to an Operator, the update callback's first
parameter will be an OperatorProperties instance, rather than an instance
of the operator itself. This means you can't access other internal functions
of the operator, only its other properties.
"""
import bpy
def update_func(self, context):
print("my test function", self)
bpy.types.Scene.testprop = bpy.props.FloatProperty(update=update_func)
bpy.context.scene.testprop = 11.0
# >>> my test function <bpy_struct, Scene("Scene")>

View File

@@ -0,0 +1,205 @@
"""
Getter/Setter Example
+++++++++++++++++++++
Accessor functions can be used for boolean, int, float, string and enum properties.
If ``get`` or ``set`` callbacks are defined, the property will not be stored in the ID properties
automatically. Instead, the ``get`` and ``set`` functions will be called when the property
is respectively read or written from the API, and are responsible to handle the data storage.
Note that:
- It is illegal to define a ``set`` callback without a matching ``get`` one.
- When a ``get`` callback is defined but no ``set`` one, the property is read-only.
``get_transform`` and ``set_transform`` can be used when the returned value needs to be modified,
but the default internal storage is still used. They can only transform the value before it is
set or returned, but do not control how/where that data is stored.
.. note::
It is possible to define both ``get``/``set`` and ``get_transform``/``set_transform`` callbacks
for the same property. In practice however, this should rarely be needed, as most 'transform'
operation can also happen within a ``get``/``set`` callback.
.. warning::
Remember that these callbacks may be executed in threaded context.
.. warning::
Take care when accessing other properties in these callbacks, as it can easily trigger
complex issues, such as infinite loops (if e.g. two properties try to also set the other
property's value in their own ``set`` callback), or unexpected side effects due to changes
in data, caused e.g. by an ``update`` callback.
"""
import bpy
scene = bpy.context.scene
# Simple property reading/writing from 'custom' IDProperties.
# This is similar to what the RNA would do internally, albeit using it own separate,
# internal 'system' IDProperty storage, since Blender 5.0.
def get_float(self):
return self.get("testprop", 0.0)
def set_float(self, value):
self["testprop"] = value
bpy.types.Scene.test_float = bpy.props.FloatProperty(get=get_float, set=set_float)
# Testing the property:
print("test_float:", scene.test_float)
scene.test_float = 7.5
print("test_float:", scene.test_float)
# The above outputs:
# test_float: 0.0
# test_float: 7.5
# Read-only string property, returns the current date.
def get_date(self):
import datetime
return str(datetime.datetime.now())
bpy.types.Scene.test_date = bpy.props.StringProperty(get=get_date)
# Testing the property:
# scene.test_date = "blah" # This would fail, property is read-only.
print("test_date:", scene.test_date)
# The above outputs something like:
# test_date: 2018-03-14 11:36:53.158653
# Boolean array.
# - Set function stores a single boolean value, returned as the second component.
# - Array getters must return a list or tuple.
# - Array size must match the property vector size exactly.
def get_array(self):
return (True, self.get("somebool", True))
def set_array(self, values):
self["somebool"] = values[0] and values[1]
bpy.types.Scene.test_array = bpy.props.BoolVectorProperty(size=2, get=get_array, set=set_array)
# Testing the property:
print("test_array:", tuple(scene.test_array))
scene.test_array = (True, False)
print("test_array:", tuple(scene.test_array))
# The above outputs:
# test_array: (True, True)
# test_array: (True, False)
# Boolean array, using 'transform' accessors.
# Note how the same result is achieved as with previous get/set example, but using default RNA storage.
# Transform accessors also have access to more information.
# Also note how the stored data _is_ a two-items array.
# - Set function stores a single boolean value, returned as the second component.
# - Array getters must return a list or tuple.
# - Array size must match the property vector size exactly.
def get_array_transform(self, curr_value, is_set):
print("Stored data:", curr_value, "(is set:", is_set, ")")
return (True, curr_value[1])
def set_array_transform(self, new_value, curr_value, is_set):
print("New data:", new_value, "; Stored data:", curr_value, "(is set:", is_set, ")")
return True, new_value[0] and new_value[1]
bpy.types.Scene.test_array_transform = bpy.props.BoolVectorProperty(
size=2, get_transform=get_array_transform, set_transform=set_array_transform)
# Testing the property:
print("test_array_transform:", tuple(scene.test_array_transform))
scene.test_array_transform = (True, False)
print("test_array_transform:", tuple(scene.test_array_transform))
# The above outputs:
# Stored data: (False, False) (is set: False )
# test_array_transform: (True, False)
# New data: (True, False) ; Stored data: (False, False) (is set: False )
# Stored data: (True, False) (is set: True )
# test_array_transform: (True, False)
# Enum property.
# Note: the getter/setter callback must use integer identifiers!
test_items = [
("RED", "Red", "", 1),
("GREEN", "Green", "", 2),
("BLUE", "Blue", "", 3),
("YELLOW", "Yellow", "", 4),
]
def get_enum(self):
import random
return random.randint(1, 4)
def set_enum(self, value):
print("setting value", value)
bpy.types.Scene.test_enum = bpy.props.EnumProperty(items=test_items, get=get_enum, set=set_enum)
# Testing the property:
print("test_enum:", scene.test_enum)
scene.test_enum = 'BLUE'
print("test_enum:", scene.test_enum)
# The above outputs something like:
# test_enum: YELLOW
# setting value 3
# test_enum: GREEN
# String, using 'transform' accessors to validate data before setting/returning it.
def get_string_transform(self, curr_value, is_set):
import os
is_valid_path = os.path.exists(curr_value)
print("Stored data:", curr_value, "(is set:", is_set, ", is valid path:", is_valid_path, ")")
return curr_value if is_valid_path else ""
def set_string_transform(self, new_value, curr_value, is_set):
import os
is_valid_path = os.path.exists(new_value)
print("New data:", new_value, "(is_valid_path:", is_valid_path, ");",
"Stored data:", curr_value, "(is set:", is_set, ")")
return new_value if is_valid_path else curr_value
bpy.types.Scene.test_string_transform = bpy.props.StringProperty(
subtype='DIR_PATH',
default="an/invalid/path",
get_transform=get_string_transform,
set_transform=set_string_transform,
)
# Testing the property:
print("test_string_transform:", scene.test_string_transform)
scene.test_string_transform = "try\\to\\find\\me"
print("test_string_transform:", scene.test_string_transform)
# The above outputs something like:
# Stored data: an/invalid/path (is set: False , is valid path: False )
# test_string_transform:
# New data: try\to\find\me (is_valid_path: False ) ; Stored data: an/invalid/path (is set: False )
# Stored data: an/invalid/path (is set: True , is valid path: False )
# test_string_transform:

View File

@@ -0,0 +1,30 @@
"""
Action Slots organize animation data within an action. Each action has slots with specific animation
data. An animated data-block specifies an action and a slot, determining the animation data it uses.
See the `Blender Manual <https://docs.blender.org/manual/en/5.1/animation/actions.html#action-slots>`_
for how Action Slots are used, or the
`technical documentation <https://developer.blender.org/docs/features/animation/>`_
for details on the animation system's architecture.
Create & Access an Action Slot
++++++++++++++++++++++++++++++
To get started with Action Slots, you can easily create them by inserting a keyframe on an object. When you do this,
Blender automatically creates an Action & Slot for that data-block.
"""
import bpy
# Assume Suzanne mesh is present in the scene.
suzanne = bpy.data.objects["Suzanne"]
# Create animation data and an action for Suzanne:
# Slot will be automatically created.
suzanne.keyframe_insert("location", index=0)
# Action slots can be accessed like this:
action = suzanne.animation_data.action
for slot in action.slots:
print(f"Slot Identifier {slot.identifier!r} "
f"with name {slot.name_display!r} "
f"targets ID type {slot.target_id_type!r}")

View File

@@ -0,0 +1,23 @@
"""
Manually Create an Action Slot
++++++++++++++++++++++++++++++
If required you can also manually create Action Slots on an Action. Note the ``target_id_type``
that matches the data-block type. Identifiers start with a prefix based on the ID type,
e.g. "OB" for objects, followed by the name. There can be identifiers like ``OBSuzanne``
and ``MESuzanne`` and the name (``Suzanne``) can be shared between them. This is intentional,
so that the slots and the datablocks can have the same name.
"""
import bpy
# Actions creation.
action = bpy.data.actions.new("SuzanneAction")
# Creation of slots requires an ID type and a name.
slot = action.slots.new(id_type='OBJECT', name="Suzanne")
print(f"slot type={slot.target_id_type!r} "
f"name={slot.name_display!r} "
f"identifier={slot.identifier!r}")
# Output:
# slot type=OBJECT name=Suzanne identifier=OBSuzanne

View File

@@ -0,0 +1,24 @@
"""
Explicitly Assigning Action Slots
+++++++++++++++++++++++++++++++++
An action slot is compatible with a data-block if the slot's ``target_id_type`` matches the data-block's type.
If there are multiple slots on the Action, and you want to just pick the first one that's
compatible, use the following code. ``anim_data.action_suitable_slots`` can be used `after` the
Action has been assigned; it is a list of action slots of that Action, but only the ones that
are actually compatible with the owner of anim_data (in this case, Suzanne).
"""
import bpy
# Assume Suzanne mesh is present in the scene.
suzanne = bpy.data.objects["Suzanne"]
# Create an action with an object slot.
action = bpy.data.actions.new("SuzanneAction")
action.slots.new(id_type='OBJECT', name="Suzanne")
# If there are multiple slots on the Action, pick the first one that's compatible.
anim_data = suzanne.animation_data_create()
anim_data.action = action
assert anim_data.action_suitable_slots, "expecting at least one suitable slot"
anim_data.action_slot = anim_data.action_suitable_slots[0]

View File

@@ -0,0 +1,17 @@
"""
Finding Action Slot Users
+++++++++++++++++++++++++
To return a list of the data-blocks that are animated by a specific slot of an Action,
use the ``users()`` method of the ActionSlot.
"""
import bpy
# Iterate through all actions in the Blender data.
print("Action & slot users:")
for action in bpy.data.actions:
for slot in action.slots:
# Return the data-blocks that are animated by this slot of this action
users = slot.users()
print(f"{action.name:20} slot={slot.identifier:12s} users: {users}")

View File

@@ -0,0 +1,73 @@
bl_info = {
"name": "Example Add-on Preferences",
"author": "Your Name Here",
"version": (1, 0),
"blender": (2, 65, 0),
"location": "SpaceBar Search -> Add-on Preferences Example",
"description": "Example Add-on",
"warning": "",
"doc_url": "",
"tracker_url": "",
"category": "Object",
}
import bpy
from bpy.types import Operator, AddonPreferences
from bpy.props import StringProperty, IntProperty, BoolProperty
class ExampleAddonPreferences(AddonPreferences):
# This must match the add-on name, use `__package__`
# when defining this for add-on extensions or a sub-module of a Python package.
bl_idname = __name__
filepath: StringProperty(
name="Example File Path",
subtype='FILE_PATH',
)
number: IntProperty(
name="Example Number",
default=4,
)
boolean: BoolProperty(
name="Example Boolean",
default=False,
)
def draw(self, context):
layout = self.layout
layout.label(text="This is a preferences view for our add-on")
layout.prop(self, "filepath")
layout.prop(self, "number")
layout.prop(self, "boolean")
class OBJECT_OT_addon_prefs_example(Operator):
"""Display example preferences"""
bl_idname = "object.addon_prefs_example"
bl_label = "Add-on Preferences Example"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
preferences = context.preferences
addon_prefs = preferences.addons[__name__].preferences
info = "Path: {:s}, Number: {:d}, Boolean {!r}".format(
addon_prefs.filepath, addon_prefs.number, addon_prefs.boolean,
)
self.report({'INFO'}, info)
print(info)
return {'FINISHED'}
# Registration
def register():
bpy.utils.register_class(OBJECT_OT_addon_prefs_example)
bpy.utils.register_class(ExampleAddonPreferences)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_addon_prefs_example)
bpy.utils.unregister_class(ExampleAddonPreferences)

View File

@@ -0,0 +1,114 @@
"""
Attributes are used to store data that corresponds to geometry elements.
Geometry elements are items in one of the geometry domains like points, curves, or faces.
An attribute has a ``name``, a ``type``, and is stored on a ``domain``.
``name``
The name of this attribute. Names have to be unique within the same geometry.
If the name starts with a ``.``, the attribute is hidden from the UI.
``type``
The type of data that this attribute stores, e.g. a float, integer, color, etc.
See `Attribute Type Items <bpy_types_enum_items/attribute_type_items.html>`__.
``domain``
The geometry domain that the attribute is stored on.
See `Attribute Domain Items <bpy_types_enum_items/attribute_domain_items.html>`__.
Using Attributes
++++++++++++++++
Attributes can be stored on geometries like :class:`Mesh`, :class:`Curves`, :class:`PointCloud`, etc.
These geometries have attribute groups (usually called ``attributes``).
Using the groups, attributes can then be accessed by their name:
.. code-block:: python
radii = curves.attributes["radius"]
Creating and storing custom attributes is done using the ``attributes.new`` function:
.. code-block:: python
# Add a new attribute named `my_attribute_name` of type `float` on the point domain of the geometry.
my_attribute = curves.attributes.new("my_attribute_name", 'FLOAT', 'POINT')
Removing attributes can be done like so:
.. code-block:: python
attribute = drawing.attributes["some_attribute"]
drawing.attributes.remove(attribute)
.. note::
Some attributes are required and cannot be removed, like ``"position"``.
Attribute values are read by accessing their ``attribute.data`` collection property.
However, in cases where multiple values should be read at once,
it is better to use the :class:`bpy_prop_collection.foreach_get` function and read the values into a ``numpy`` buffer.
.. code-block:: python
import numpy as np
# Get the radius attribute.
radii = curves.attributes["radius"]
# Print the radius of the first point.
print(radii.data[0].value)
# Output: 0.005
# Get the total number of points.
num_points = attributes.domain_size('POINT')
# Create an empty buffer to read all the radii into.
radii_data = np.zeros(num_points, dtype=np.float32)
# Read all the radii of the curves into `radii_data` at once.
radii.data.foreach_get('value', radii_data)
# Print all the radii.
print(radii_data)
# Output: [0.1, 0.2, 0.3, 0.4, ... ]
.. note::
Some attribute types use different named properties to access their value.
Instead of ``value``, vectors use ``vector``, and colors use ``color``.
Writing to different attribute types is very similar. You can simply assign to a value directly.
Again, when writing to multiple values, it is recommended to use the :class:`bpy_prop_collection.foreach_set` function
to write the values from a ``numpy`` buffer.
.. code-block:: python
import numpy as np
radii = curves.attributes["radius"]
# Write a radius with a value of 0.5 to the first point.
radii.data[0].value = 0.5
print(radii.data[0].value)
# Output: 0.5
num_points = attributes.domain_size('POINT')
# Generate random radii with values between 0.001 and 0.05 using numpy.
new_radii = np.random.uniform(0.001, 0.05, num_points)
# Write the new radii to the radius attribute.
radii.data.foreach_set('value', new_radii)
The :class:`bpy_prop_collection.foreach_get` / :class:`bpy_prop_collection.foreach_set` methods require a flat array.
This is sometimes not desirable, e.g. when reading/writing positions, which are 3D vectors.
In these cases, it's possible to use ``np.ravel`` to pass the data as a flat array:
.. code-block:: python
num_points = attributes.domain_size('POINT')
positions = curves.attributes['position']
# Here, we're using a numpy array with shape (num_points, 3) so that each
# element is a 3d vector.
positions_data = np.zeros((num_points, 3), dtype=np.float32)
# The `np.ravel` function will pass the `positions_data` as a flat array
# without changing the original shape.
positions.data.foreach_get('vector', np.ravel(positions_data))
print(positions_data)
# Output: [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], ...]
"""

View File

@@ -0,0 +1,34 @@
import bpy
filepath = "//link_library.blend"
# Load a single scene we know the name of.
with bpy.data.libraries.load(filepath) as (data_src, data_dst):
data_dst.scenes = ["Scene"]
# Load all meshes.
with bpy.data.libraries.load(filepath) as (data_src, data_dst):
data_dst.meshes = data_src.meshes
# Link all objects starting with "A".
with bpy.data.libraries.load(filepath, link=True) as (data_src, data_dst):
data_dst.objects = [name for name in data_src.objects if name.startswith("A")]
# Append everything.
with bpy.data.libraries.load(filepath) as (data_src, data_dst):
for attr in dir(data_dst):
setattr(data_dst, attr, getattr(data_src, attr))
# The loaded objects can be accessed from `data_dst` outside of the context
# since loading the data replaces the strings for the data-blocks or None
# if the data-block could not be loaded.
with bpy.data.libraries.load(filepath) as (data_src, data_dst):
data_dst.meshes = data_src.meshes
# Now operate directly on the loaded data.
for mesh in data_dst.meshes:
if mesh is not None:
print(mesh.name)

View File

@@ -0,0 +1,18 @@
import bpy
filepath = "//new_library.blend"
# Write selected objects and their data to a blend file.
data_blocks = set(bpy.context.selected_objects)
bpy.data.libraries.write(filepath, data_blocks)
# Write all meshes starting with a capital letter and
# set them with fake-user enabled so they aren't lost on re-saving.
data_blocks = {mesh for mesh in bpy.data.meshes if mesh.name[:1].isupper()}
bpy.data.libraries.write(filepath, data_blocks, fake_user=True)
# Write all materials, textures and node groups to a library.
data_blocks = {*bpy.data.materials, *bpy.data.textures, *bpy.data.node_groups}
bpy.data.libraries.write(filepath, data_blocks)

View File

@@ -0,0 +1,56 @@
"""
This method enables conversions between Local and Pose space for bones in
the middle of updating the armature without having to update dependencies
after each change, by manually carrying updated matrices in a recursive walk.
"""
def set_pose_matrices(obj, matrix_map):
"Assign pose space matrices of all bones at once, ignoring constraints."
def rec(pbone, parent_matrix):
if pbone.name in matrix_map:
matrix = matrix_map[pbone.name]
# # Instead of:
# pbone.matrix = matrix
# bpy.context.view_layer.update()
# Compute and assign local matrix, using the new parent matrix.
if pbone.parent:
pbone.matrix_basis = pbone.bone.convert_local_to_pose(
matrix,
pbone.bone.matrix_local,
parent_matrix=parent_matrix,
parent_matrix_local=pbone.parent.bone.matrix_local,
invert=True
)
else:
pbone.matrix_basis = pbone.bone.convert_local_to_pose(
matrix,
pbone.bone.matrix_local,
invert=True
)
else:
# Compute the updated pose matrix from local and new parent matrix.
if pbone.parent:
matrix = pbone.bone.convert_local_to_pose(
pbone.matrix_basis,
pbone.bone.matrix_local,
parent_matrix=parent_matrix,
parent_matrix_local=pbone.parent.bone.matrix_local,
)
else:
matrix = pbone.bone.convert_local_to_pose(
pbone.matrix_basis,
pbone.bone.matrix_local,
)
# Recursively process children, passing the new matrix through.
for child in pbone.children:
rec(child, matrix)
# Scan all bone trees from their roots.
for pbone in obj.pose.bones:
if not pbone.parent:
rec(pbone, None)

View File

@@ -0,0 +1,19 @@
"""
Overriding the context can be used to temporarily activate another ``window`` / ``area`` & ``region``,
as well as other members such as the ``active_object`` or ``bone``.
Notes:
- When overriding window, area and regions: the arguments must be consistent,
so any region argument that's passed in must be contained by the current area or the area passed in.
The same goes for the area needing to be contained in the current window.
- Temporary context overrides may be nested, when this is done, members will be added to the existing overrides.
- Context members are restored outside the scope of the context-manager.
The only exception to this is when the data is no longer available.
In the event windowing data was removed (for example), the state of the context is left as-is.
While this isn't likely to happen, explicit window operation such as closing windows or loading a new file
remove the windowing data that was set before the temporary context was created.
"""

View File

@@ -0,0 +1,15 @@
"""
Overriding the context can be useful to set the context after loading files
(which would otherwise be None). For example:
"""
import bpy
from bpy import context
# Reload the current file and select all.
bpy.ops.wm.open_mainfile(filepath=bpy.data.filepath)
window = context.window_manager.windows[0]
with context.temp_override(window=window):
bpy.ops.mesh.primitive_uv_sphere_add()
# The context override is needed so it's possible to set edit-mode.
bpy.ops.object.mode_set(mode='EDIT')

View File

@@ -0,0 +1,16 @@
"""
This example shows how it's possible to add an object to the scene in another window.
"""
import bpy
from bpy import context
win_active = context.window
win_other = None
for win_iter in context.window_manager.windows:
if win_iter != win_active:
win_other = win_iter
break
# Add cube in the other window.
with context.temp_override(window=win_other):
bpy.ops.mesh.primitive_cube_add()

View File

@@ -0,0 +1,30 @@
"""
**Logging Context Member Access**
Context members can be logged by calling ``logging_set(True)`` on the "with" target of a temporary override.
This will log the members that are being accessed during the operation and may
assist in debugging when it is unclear which members need to be overridden.
In the event an operator fails to execute because of a missing context member, logging may help
identify which member is required.
This example shows how to log which context members are being accessed.
Log statements are printed to your system's console.
.. important::
Not all operators rely on Context Members and therefore will not be affected by
:class:`bpy.types.Context.temp_override`, use logging to what members if any are accessed.
"""
import bpy
from bpy import context
my_objects = [context.scene.camera]
with context.temp_override(selected_objects=my_objects) as override:
override.logging_set(
True, # Enable logging.
hide_missing=True, # Don't show failed attempts.
)
bpy.ops.object.delete()

View File

@@ -0,0 +1,60 @@
"""
Dependency graph: Evaluated ID example
++++++++++++++++++++++++++++++++++++++
This example demonstrates access to the evaluated ID (such as object, material, etc.) state from
an original ID.
This is needed every time one needs to access state with animation, constraints, and modifiers
taken into account.
"""
import bpy
class OBJECT_OT_evaluated_example(bpy.types.Operator):
"""Access evaluated object state and do something with it"""
bl_label = "DEG Access Evaluated Object"
bl_idname = "object.evaluated_example"
def execute(self, context):
# This is an original object. Its data does not have any modifiers applied.
obj = context.object
if obj is None or obj.type != 'MESH':
self.report({'INFO'}, "No active mesh object to get info from")
return {'CANCELLED'}
# Evaluated object exists within a specific dependency graph.
# We will request evaluated object from the dependency graph which corresponds to the
# current scene and view layer.
#
# NOTE: This call ensure the dependency graph is fully evaluated. This might be expensive
# if changes were made to the scene, but is needed to ensure no dangling or incorrect
# pointers are exposed.
depsgraph = context.evaluated_depsgraph_get()
# Actually request evaluated object.
#
# This object has animation and drivers applied on it, together with constraints and
# modifiers.
#
# For mesh objects the object.data will be a mesh with all modifiers applied.
# This means that in access to vertices or faces after modifier stack happens via fields of
# object_eval.object.
#
# For other types of objects the object_eval.data does not have modifiers applied on it,
# but has animation applied.
#
# NOTE: All ID types have `evaluated_get()`, including materials, node trees, worlds.
object_eval = obj.evaluated_get(depsgraph)
mesh_eval = object_eval.data
self.report({'INFO'}, f"Number of evaluated vertices: {len(mesh_eval.vertices)}")
return {'FINISHED'}
def register():
bpy.utils.register_class(OBJECT_OT_evaluated_example)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_evaluated_example)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,45 @@
"""
Dependency graph: Original object example
+++++++++++++++++++++++++++++++++++++++++
This example demonstrates access to the original ID.
Such access is needed to check whether object is selected, or to compare pointers.
"""
import bpy
class OBJECT_OT_original_example(bpy.types.Operator):
"""Access original object and do something with it"""
bl_label = "DEG Access Original Object"
bl_idname = "object.original_example"
def check_object_selected(self, object_eval):
# Selection depends on a context and is only valid for original objects. This means we need
# to request the original object from the known evaluated one.
#
# NOTE: All ID types have an `original` field.
obj = object_eval.original
return obj.select_get()
def execute(self, context):
# NOTE: It seems redundant to iterate over original objects to request evaluated ones
# just to get original back. But we want to keep example as short as possible, but in real
# world there are cases when evaluated object is coming from a more meaningful source.
depsgraph = context.evaluated_depsgraph_get()
for obj in context.editable_objects:
object_eval = obj.evaluated_get(depsgraph)
if self.check_object_selected(object_eval):
self.report({'INFO'}, f"Object is selected: {object_eval.name}")
return {'FINISHED'}
def register():
bpy.utils.register_class(OBJECT_OT_original_example)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_original_example)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,42 @@
"""
Dependency graph: Iterate over all object instances
+++++++++++++++++++++++++++++++++++++++++++++++++++
Sometimes it is needed to know all the instances with their matrices (for example, when writing an
exporter or a custom render engine).
This example shows how to access all objects and instances in the scene.
"""
import bpy
class OBJECT_OT_object_instances(bpy.types.Operator):
"""Access original object and do something with it"""
bl_label = "DEG Iterate Object Instances"
bl_idname = "object.object_instances"
def execute(self, context):
depsgraph = context.evaluated_depsgraph_get()
for object_instance in depsgraph.object_instances:
# This is an object which is being instanced.
obj = object_instance.object
# `is_instance` denotes whether the object is coming from instances (as an opposite of
# being an emitting object. )
if not object_instance.is_instance:
print(f"Object {obj.name} at {object_instance.matrix_world}")
else:
# Instanced will additionally have fields like uv, random_id and others which are
# specific for instances. See Python API for DepsgraphObjectInstance for details,
print(f"Instance of {obj.name} at {object_instance.matrix_world}")
return {'FINISHED'}
def register():
bpy.utils.register_class(OBJECT_OT_object_instances)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_object_instances)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,69 @@
"""
Dependency graph: Object.to_mesh()
+++++++++++++++++++++++++++++++++++
Function to get a mesh from any object with geometry. It is typically used by exporters, render
engines and tools that need to access the evaluated mesh as displayed in the viewport.
Object.to_mesh() is closely interacting with dependency graph: its behavior depends on whether it
is used on original or evaluated object.
When is used on original object, the result mesh is calculated from the object without taking
animation or modifiers into account:
- For meshes this is similar to duplicating the source mesh.
- For curves this disables own modifiers, and modifiers of objects used as bevel and taper.
- For meta-balls this produces an empty mesh since polygonization is done as a modifier evaluation.
When is used on evaluated object all modifiers are taken into account.
.. note:: The result mesh is owned by the object. It can be freed by calling :meth:`~Object.to_mesh_clear`.
.. note::
The result mesh must be treated as temporary, and cannot be referenced from objects in the main
database. If the mesh intended to be used in a persistent manner use :meth:`~BlendDataMeshes.new_from_object`
instead.
.. note:: If object does not have geometry (i.e. camera) the functions returns None.
"""
import bpy
class OBJECT_OT_object_to_mesh(bpy.types.Operator):
"""Convert selected object to mesh and show number of vertices"""
bl_label = "DEG Object to Mesh"
bl_idname = "object.object_to_mesh"
def execute(self, context):
# Access input original object.
obj = context.object
if obj is None:
self.report({'INFO'}, "No active mesh object to convert to mesh")
return {'CANCELLED'}
# Avoid annoying None checks later on.
if obj.type not in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
self.report({'INFO'}, "Object cannot be converted to mesh")
return {'CANCELLED'}
depsgraph = context.evaluated_depsgraph_get()
# Invoke to_mesh() for original object.
mesh_from_orig = obj.to_mesh()
self.report({'INFO'}, f"{len(mesh_from_orig.vertices)} in new mesh without modifiers.")
# Remove temporary mesh.
obj.to_mesh_clear()
# Invoke to_mesh() for evaluated object.
object_eval = obj.evaluated_get(depsgraph)
mesh_from_eval = object_eval.to_mesh()
self.report({'INFO'}, f"{len(mesh_from_eval.vertices)} in new mesh with modifiers.")
# Remove temporary mesh.
object_eval.to_mesh_clear()
return {'FINISHED'}
def register():
bpy.utils.register_class(OBJECT_OT_object_to_mesh)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_object_to_mesh)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,57 @@
"""
Dependency graph: bpy.data.meshes.new_from_object()
+++++++++++++++++++++++++++++++++++++++++++++++++++
Function to copy a new mesh from any object with geometry. The mesh is added to the main
database and can be referenced by objects. Typically used by tools that create new objects
or apply modifiers.
When is used on original object, the result mesh is calculated from the object without taking
animation or modifiers into account:
- For meshes this is similar to duplicating the source mesh.
- For curves this disables own modifiers, and modifiers of objects used as bevel and taper.
- For meta-balls this produces an empty mesh since polygonization is done as a modifier evaluation.
When is used on evaluated object all modifiers are taken into account.
All the references (such as materials) are re-mapped to original. This ensures validity and
consistency of the main database.
.. note:: If object does not have geometry (i.e. camera) the functions returns None.
"""
import bpy
class OBJECT_OT_mesh_from_object(bpy.types.Operator):
"""Convert selected object to mesh and show number of vertices"""
bl_label = "DEG Mesh From Object"
bl_idname = "object.mesh_from_object"
def execute(self, context):
# Access input original object.
obj = context.object
if obj is None:
self.report({'INFO'}, "No active mesh object to convert to mesh")
return {'CANCELLED'}
# Avoid annoying None checks later on.
if obj.type not in {'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'}:
self.report({'INFO'}, "Object cannot be converted to mesh")
return {'CANCELLED'}
depsgraph = context.evaluated_depsgraph_get()
object_eval = obj.evaluated_get(depsgraph)
mesh_from_eval = bpy.data.meshes.new_from_object(object_eval)
self.report({'INFO'}, f"{len(mesh_from_eval.vertices)} in new mesh, and is ready for use!")
return {'FINISHED'}
def register():
bpy.utils.register_class(OBJECT_OT_mesh_from_object)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_mesh_from_object)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,68 @@
"""
Dependency graph: Simple exporter
+++++++++++++++++++++++++++++++++
This example is a combination of all previous ones, and shows how to write a simple exporter
script.
"""
import bpy
class OBJECT_OT_simple_exporter(bpy.types.Operator):
"""Simple (fake) exporter of selected objects"""
bl_label = "DEG Export Selected"
bl_idname = "object.simple_exporter"
apply_modifiers: bpy.props.BoolProperty(name="Apply Modifiers")
def execute(self, context):
depsgraph = context.evaluated_depsgraph_get()
for object_instance in depsgraph.object_instances:
if not self.is_object_instance_from_selected(object_instance):
# We only export selected objects.
continue
# NOTE: This will create a mesh for every instance, which is not ideal at all. In
# reality destination format will support some sort of instancing mechanism, so the
# code here will simply say "instance this object at object_instance.matrix_world".
mesh = self.create_mesh_for_object_instance(object_instance)
if mesh is None:
# Happens for non-geometry objects.
continue
print(f"Exporting mesh with {len(mesh.vertices)} vertices "
f"at {object_instance.matrix_world}")
self.clear_mesh_for_object_instance(object_instance)
return {'FINISHED'}
def is_object_instance_from_selected(self, object_instance):
# For instanced objects we check selection of their instancer (more accurately: check
# selection status of the original object corresponding to the instancer).
if object_instance.parent:
return object_instance.parent.original.select_get()
# For non-instanced objects we check selection state of the original object.
return object_instance.object.original.select_get()
def create_mesh_for_object_instance(self, object_instance):
if self.apply_modifiers:
return object_instance.object.to_mesh()
else:
return object_instance.object.original.to_mesh()
def clear_mesh_for_object_instance(self, object_instance):
if self.apply_modifiers:
return object_instance.object.to_mesh_clear()
else:
return object_instance.object.original.to_mesh_clear()
def register():
bpy.utils.register_class(OBJECT_OT_simple_exporter)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_simple_exporter)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,63 @@
"""
Dependency graph: Object.to_curve()
+++++++++++++++++++++++++++++++++++
Function to get a curve from text and curve objects. It is typically used by exporters, render
engines, and tools that need to access the curve representing the object.
The function takes the evaluated dependency graph as a required parameter and optionally a boolean
apply_modifiers which defaults to false. If apply_modifiers is true and the object is a curve object,
the spline deform modifiers are applied on the control points. Note that constructive modifiers and
modifiers that are not spline-enabled will not be applied. So modifiers like Array will not be applied
and deform modifiers that have Apply On Spline disabled will not be applied.
If the object is a text object. The text will be converted into a 3D curve and returned. Modifiers are
never applied on text objects and apply_modifiers will be ignored. If the object is neither a curve nor
a text object, an error will be reported.
.. note:: The resulting curve is owned by the object. It can be freed by calling :meth:`~Object.to_curve_clear`.
.. note::
The resulting curve must be treated as temporary, and cannot be referenced from objects in the main
database.
"""
import bpy
class OBJECT_OT_object_to_curve(bpy.types.Operator):
"""Convert selected object to curve and show number of splines"""
bl_label = "DEG Object to Curve"
bl_idname = "object.object_to_curve"
def execute(self, context):
# Access input original object.
obj = context.object
if obj is None:
self.report({'INFO'}, "No active object to convert to curve")
return {'CANCELLED'}
if obj.type not in {'CURVE', 'FONT'}:
self.report({'INFO'}, "Object cannot be converted to curve")
return {'CANCELLED'}
depsgraph = context.evaluated_depsgraph_get()
# Invoke to_curve() without applying modifiers.
curve_without_modifiers = obj.to_curve(depsgraph)
self.report({'INFO'}, f"{len(curve_without_modifiers.splines)} splines in a new curve without modifiers.")
# Remove temporary curve.
obj.to_curve_clear()
# Invoke to_curve() with applying modifiers.
curve_with_modifiers = obj.to_curve(depsgraph, apply_modifiers=True)
self.report({'INFO'}, f"{len(curve_with_modifiers.splines)} splines in new curve with modifiers.")
# Remove temporary curve.
obj.to_curve_clear()
return {'FINISHED'}
def register():
bpy.utils.register_class(OBJECT_OT_object_to_curve)
def unregister():
bpy.utils.unregister_class(OBJECT_OT_object_to_curve)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,81 @@
"""
Basic FileHandler for importing a single file
---------------------------------------------
A file handler allows custom drag-and-drop behavior to be associated with a given ``Operator``
(:class:`FileHandler.bl_import_operator`) and set of file extensions
(:class:`FileHandler.bl_file_extensions`). Control over which area of the UI accepts the
drag-in-drop action is specified using the :class:`FileHandler.poll_drop` method.
Similar to operators that use a file select window, operators participating in drag-and-drop, and
only accepting a single file, must define the following property:
.. code-block:: python
filepath: bpy.props.StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE'})
This ``filepath`` property will be set to the full path of the file dropped by the user.
"""
import bpy
class CurveTextImport(bpy.types.Operator):
"""
Creates a text object from a text file.
"""
bl_idname = "curve.text_import"
bl_label = "Import a text file as text object"
# This Operator supports processing one `.txt` file at a time. The following file-path
# property must be defined.
filepath: bpy.props.StringProperty(subtype='FILE_PATH', options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return (context.area and context.area.type == "VIEW_3D")
def execute(self, context):
# Direct calls to this Operator may use unsupported file-paths. Ensure the incoming
# file-path is one that is supported.
if not self.filepath or not self.filepath.endswith(".txt"):
return {'CANCELLED'}
# Create a Blender Text object from the contents of the provided file.
with open(self.filepath) as file:
text_curve = bpy.data.curves.new(type="FONT", name="Text")
text_curve.body = ''.join(file.readlines())
text_object = bpy.data.objects.new(name="Text", object_data=text_curve)
bpy.context.scene.collection.objects.link(text_object)
return {'FINISHED'}
# By default the file handler invokes the operator with the file-path property set. If the
# operator also supports being invoked with no file-path set, and allows the user to pick from a
# file select window instead, the following logic can be used.
#
# Note: It is important to use `options={'SKIP_SAVE'}` when defining the file-path property to
# avoid prior values from being reused on subsequent calls.
def invoke(self, context, event):
if self.filepath:
return self.execute(context)
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
# Define a file handler that supports the following set of conditions:
# - Execute the `curve.text_import` operator
# - When `.txt` files are dropped in the 3D Viewport
class CURVE_FH_text_import(bpy.types.FileHandler):
bl_idname = "CURVE_FH_text_import"
bl_label = "File handler for curve text object import"
bl_import_operator = "curve.text_import"
bl_file_extensions = ".txt"
@classmethod
def poll_drop(cls, context):
return (context.area and context.area.type == 'VIEW_3D')
bpy.utils.register_class(CurveTextImport)
bpy.utils.register_class(CURVE_FH_text_import)

View File

@@ -0,0 +1,109 @@
"""
FileHandler for Importing multiple files and exposing Operator options
----------------------------------------------------------------------
Operators which support being executed with multiple files from drag-and-drop require the
following properties be defined:
.. code-block:: python
directory: StringProperty(subtype='DIR_PATH', options={'SKIP_SAVE', 'HIDDEN'})
files: CollectionProperty(type=OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'})
These ``directory`` and ``files`` properties will be set with the necessary data from the
drag-and-drop operation.
Additionally, if the operator provides operator properties that need to be accessible to the user,
the :class:`ImportHelper.invoke_popup` method can be used to show a dialog leveraging the standard
:class:`Operator.draw` method for layout and display.
"""
import bpy
from bpy_extras.io_utils import ImportHelper
from mathutils import Vector
class ShaderScriptImport(bpy.types.Operator, ImportHelper):
"""
Creates one or more Shader Script nodes from text files.
"""
bl_idname = "shader.script_import"
bl_label = "Import a text file as a script node"
# This Operator supports processing multiple `.txt` files at a time. The following properties
# must be defined.
directory: bpy.props.StringProperty(subtype='DIR_PATH', options={'SKIP_SAVE', 'HIDDEN'})
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'SKIP_SAVE', 'HIDDEN'})
# Allow the user to choose whether the node's label is set or not
set_label: bpy.props.BoolProperty(name="Set Label", default=False)
@classmethod
def poll(cls, context):
return (
context.region and context.region.type == 'WINDOW' and
context.area and context.area.ui_type == 'ShaderNodeTree' and
context.object and context.object.type == 'MESH' and
context.material
)
def execute(self, context):
# The directory property must be set.
if not self.directory:
return {'CANCELLED'}
x = 0.0
y = 0.0
for file in self.files:
# Direct calls to this Operator may use unsupported file-paths. Ensure the incoming
# files are ones that are supported.
if file.name.endswith(".txt"):
import os
filepath = os.path.join(self.directory, file.name)
node_tree = context.material.node_tree
text_node = node_tree.nodes.new(type="ShaderNodeScript")
text_node.mode = 'EXTERNAL'
text_node.filepath = filepath
text_node.location = Vector((x, y))
# Set the node's title to the file name.
if self.set_label:
text_node.label = file.name
x += 20.0
y -= 20.0
return {'FINISHED'}
# Use ImportHelper's invoke_popup() to handle the invocation so that this operator's properties
# are shown in a popup. This allows the user to configure additional settings on the operator
# like the `set_label` property. Consider having a draw() method on the operator in order to
# layout the properties in the UI appropriately.
#
# If filepath information is not provided the file select window will be invoked instead.
def invoke(self, context, event):
return self.invoke_popup(context)
# Define a file handler that supports the following set of conditions:
# - Execute the `shader.script_import` operator
# - When `.txt` files are dropped in the Shader Editor
class SHADER_FH_script_import(bpy.types.FileHandler):
bl_idname = "SHADER_FH_script_import"
bl_label = "File handler for shader script node import"
bl_import_operator = "shader.script_import"
bl_file_extensions = ".txt"
@classmethod
def poll_drop(cls, context):
return (
context.region and context.region.type == 'WINDOW' and
context.area and context.area.ui_type == 'ShaderNodeTree'
)
bpy.utils.register_class(ShaderScriptImport)
bpy.utils.register_class(SHADER_FH_script_import)

View File

@@ -0,0 +1,53 @@
"""
Accessing Evaluated Geometry
++++++++++++++++++++++++++++
"""
import bpy
# The GeometrySet can only be retrieved from an evaluated object. So one always
# needs a depsgraph that has evaluated the object.
depsgraph = bpy.context.view_layer.depsgraph
ob = bpy.context.active_object
ob_eval = depsgraph.id_eval_get(ob)
# Get the final evaluated geometry of an object.
geometry = ob_eval.evaluated_geometry()
# Print basic information like the number of elements.
print(geometry)
# A geometry set may have a name. It can be set with the Set Geometry Name node.
print(geometry.name)
# Access "realized" geometry components.
print(geometry.mesh)
print(geometry.pointcloud)
print(geometry.curves)
print(geometry.volume)
print(geometry.grease_pencil)
# Access the mesh without final subdivision applied.
print(geometry.mesh_base)
# Accessing instances is a bit more tricky, because there is no specific
# mechanism to expose instances. Instead, two accessors are provided which
# are easy to keep working in the future even if we get a proper Instances type.
# This is a pointcloud that provides access to all the instance attributes.
# There is a point per instances. May return None if there is no instances data.
instances_pointcloud = geometry.instances_pointcloud()
if instances_pointcloud is not None:
# This is a list containing the data that is instanced. The list may contain
# None, objects, collections or other GeometrySets. If the geometry does not
# have instances, the list is empty.
references = geometry.instance_references()
# Besides normal generic attributes, there are also two important
# instance-specific attributes. "instance_transform" is a 4x4 matrix attribute
# containing the transforms of each instance.
instance_transforms = instances_pointcloud.attributes["instance_transform"]
# ".reference_index" contains indices into the `references` list above and
# determines what geometry each instance uses.
reference_indices = instances_pointcloud.attributes[".reference_index"]

View File

@@ -0,0 +1,61 @@
"""
Base class for integrating USD Hydra based renderers.
USD Hydra Based Renderer
++++++++++++++++++++++++
"""
import bpy
class CustomHydraRenderEngine(bpy.types.HydraRenderEngine):
# Identifier and name in the user interface.
bl_idname = "CUSTOM_HYDRA_RENDERER"
bl_label = "Custom Hydra Renderer"
# Name of the render plugin.
bl_delegate_id = "HdCustomRendererPlugin"
# Use MaterialX instead of `UsdPreviewSurface` for materials.
bl_use_materialx = True
# Register path to plugin.
@classmethod
def register(cls):
# Make `pxr` module available, for running as `bpy` PIP package.
bpy.utils.expose_bundled_modules()
import pxr.Plug
pxr.Plug.Registry().RegisterPlugins(['/path/to/plugin'])
# Render settings that will be passed to the delegate.
def get_render_settings(self, engine_type):
return {
'myBoolean': True,
'myValue': 8,
'aovToken:Depth': "depth",
}
# RenderEngine methods for update, render and draw are implemented in
# HydraRenderEngine. Optionally extra work can be done before or after
# by implementing the methods like this.
def update(self, data, depsgraph):
super().update(data, depsgraph)
# Do extra work here.
def update_render_passes(self, scene, render_layer):
if render_layer.use_pass_z:
self.register_pass(scene, render_layer, 'Depth', 1, 'Z', 'VALUE')
# Registration.
def register():
bpy.utils.register_class(CustomHydraRenderEngine)
def unregister():
bpy.utils.unregister_class(CustomHydraRenderEngine)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,16 @@
"""
This function is for advanced use only, misuse can crash Blender since the user
count is used to prevent data being removed when it is used.
"""
# This example shows what _not_ to do, and will crash Blender.
import bpy
# Object which is in the scene.
obj = bpy.data.objects["Cube"]
# Without this, removal would raise an error.
obj.user_clear()
# Runs without an exception but will crash on redraw.
bpy.data.objects.remove(obj)

View File

@@ -0,0 +1,46 @@
"""
Image Data
++++++++++
The Image data-block is a shallow wrapper around image or video file(s)
(on disk, as packed data, or generated).
All actual data like the pixel buffer, size, resolution etc. is
cached in an :class:`imbuf.types.ImBuf` image buffer (or several buffers
in some cases, like UDIM textures, multi-views, animations...).
Several properties and functions of the Image data-block are then actually
using/modifying its image buffer, and not the Image data-block itself.
.. warning::
One key limitation is that image buffers are not shared between different
Image data-blocks, and they are not duplicated when copying an image.
So until a modified image buffer is saved on disk, duplicating its Image
data-block will not propagate the underlying buffer changes to the new Image.
This example script generates an Image data-block with a given size,
change its first pixel, rescale it, and duplicates the image.
The duplicated image still has the same size and colors as the original image
at its creation, all editing in the original image's buffer is 'lost' in its copy.
"""
import bpy
image_src = bpy.data.images.new('src', 1024, 102)
print(image_src.size)
print(image_src.pixels[0:4])
image_src.scale(1024, 720)
image_src.pixels[0:4] = (0.5, 0.5, 0.5, 0.5)
image_src.update()
print(image_src.size)
print(image_src.pixels[0:4])
image_dest = image_src.copy()
image_dest.update()
print(image_dest.size)
print(image_dest.pixels[0:4])

View File

@@ -0,0 +1,23 @@
"""
Inline Shader Nodes
+++++++++++++++++++
"""
import bpy
# The materials should be retrieved from the evaluated object to make sure that
# e.g. edits of Geometry Nodes are applied.
depsgraph = bpy.context.view_layer.depsgraph
ob = bpy.context.active_object
ob_eval = depsgraph.id_eval_get(ob)
material_eval = ob_eval.material_slots[0].material
# Compute the inlined shader nodes.
# Important: Do not loose the reference to this object while accessing the inlined
# node tree. Otherwise there will be a crash due to a dangling pointer.
inline_shader_nodes = material_eval.inline_shader_nodes()
# Get the actual inlined `bpy.types.NodeTree`.
tree = inline_shader_nodes.node_tree
for node in tree.nodes:
print(node.name)

View File

@@ -0,0 +1,73 @@
"""
Add-on Keymap Registration
++++++++++++++++++++++++++
This example shows how an add-on can register custom keyboard shortcuts.
Keymaps are added to ``keyconfigs.addon`` and removed when unregistered.
Store ``(keymap, keymap_item)`` tuples for safe cleanup, as multiple add-ons may use the same keymap.
.. note::
Users can customize add-on shortcuts in the Keymap Preferences.
Add-on keymaps appear under their respective editors and can be
modified or disabled without editing the add-on code.
Add-ons should only manipulate keymaps in ``keyconfigs.addon`` and not manipulate the user's keymaps
because add-on keymaps serve as a default which users may customize.
Modifying user keymaps directly interferes with users' own preferences.
.. warning::
Add-ons can add items to existing modal keymaps but cannot create
new modal keymaps via Python. Use ``modal=True`` when targeting
an existing modal keymap such as "Knife Tool Modal Map".
"""
# In this example keymap registration functions are only split out for clarity,
# so skipping keymap registration in background mode doesn't interfere with other registration logic.
import bpy
# Store (keymap, keymap_item) for cleanup on unregister.
addon_keymaps = []
def register_keymaps():
wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc is None:
return # Can be None in background mode.
# Target the 3D View; name must match Blender's built-in keymap exactly.
km = kc.keymaps.new(name="3D View", space_type='VIEW_3D')
# Bind Shift+Alt+K to frame selected objects.
kmi = km.keymap_items.new(
idname="view3d.view_selected",
type='K',
value='PRESS',
shift=True,
alt=True,
)
kmi.properties.use_all_regions = True
addon_keymaps.append((km, kmi))
def unregister_keymaps():
for km, kmi in addon_keymaps:
km.keymap_items.remove(kmi)
addon_keymaps.clear()
def register():
register_keymaps()
def unregister():
unregister_keymaps()
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,50 @@
"""
Example Macro
+++++++++++++
This example creates a simple macro operator that
moves the active object and then rotates it.
It demonstrates:
- Defining a macro operator class.
- Registering it and defining sub-operators.
- Setting property values for each step.
"""
import bpy
class OBJECT_OT_simple_macro(bpy.types.Macro):
bl_idname = "object.simple_macro"
bl_label = "Simple Transform Macro"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return context.active_object is not None
def register():
bpy.utils.register_class(OBJECT_OT_simple_macro)
# Define steps after registration and set operator values via .properties
step = OBJECT_OT_simple_macro.define("transform.translate")
props = step.properties
props.value = (1.0, 0.0, 0.0)
props.constraint_axis = (True, False, False)
step = OBJECT_OT_simple_macro.define("transform.rotate")
props = step.properties
props.value = 0.785398 # 45 degrees in radians
props.orient_axis = 'Z'
def unregister():
bpy.utils.unregister_class(OBJECT_OT_simple_macro)
if __name__ == "__main__":
register()
# To run the macro:
bpy.ops.object.simple_macro()

View File

@@ -0,0 +1,42 @@
"""
Basic Menu Example
++++++++++++++++++
Here is an example of a simple menu. Menus differ from panels in that they must
reference from a header, panel or another menu.
Notice the 'CATEGORY_MT_name' in :class:`Menu.bl_idname`, this is a naming
convention for menus.
.. note::
Menu subclasses must be registered before referencing them from Blender.
.. note::
Menus have their :class:`UILayout.operator_context` initialized as
'EXEC_REGION_WIN' rather than 'INVOKE_REGION_WIN' (see :ref:`Execution Context <rna_enum_operator_context_items>`).
If the operator context needs to initialize inputs from the
:class:`Operator.invoke` function, then this needs to be explicitly set.
When a menu is added to UI elements such as a panel or header,
the operator execution context will be inherited from them.
"""
import bpy
class BasicMenu(bpy.types.Menu):
bl_idname = "OBJECT_MT_select_test"
bl_label = "Select"
def draw(self, context):
layout = self.layout
layout.operator("object.select_all", text="Select/Deselect All").action = 'TOGGLE'
layout.operator("object.select_all", text="Inverse").action = 'INVERT'
layout.operator("object.select_random", text="Random")
bpy.utils.register_class(BasicMenu)
# Test call to display immediately.
bpy.ops.wm.call_menu(name="OBJECT_MT_select_test")

View File

@@ -0,0 +1,38 @@
"""
Submenus
++++++++
This menu demonstrates some different functions.
"""
import bpy
class SubMenu(bpy.types.Menu):
bl_idname = "OBJECT_MT_select_submenu"
bl_label = "Select"
def draw(self, context):
layout = self.layout
layout.operator("object.select_all", text="Select/Deselect All").action = 'TOGGLE'
layout.operator("object.select_all", text="Inverse").action = 'INVERT'
layout.operator("object.select_random", text="Random")
# Access this operator as a sub-menu.
layout.operator_menu_enum("object.select_by_type", "type", text="Select All by Type")
layout.separator()
# Expand each operator option into this menu.
layout.operator_enum("object.light_add", "type")
layout.separator()
# Use existing menu.
layout.menu("VIEW3D_MT_transform")
bpy.utils.register_class(SubMenu)
# Test call to display immediately.
bpy.ops.wm.call_menu(name="OBJECT_MT_select_submenu")

View File

@@ -0,0 +1,18 @@
"""
Extending Menus
+++++++++++++++
When creating menus for add-ons you can't reference menus
in Blender's default scripts.
Instead, the add-on can add menu items to existing menus.
The function menu_draw acts like :class:`Menu.draw`.
"""
import bpy
def menu_draw(self, context):
self.layout.operator("wm.save_homefile")
bpy.types.TOPBAR_MT_file.append(menu_draw)

View File

@@ -0,0 +1,80 @@
"""
Preset Menus
++++++++++++
Preset menus are simply a convention that uses a menu sub-class
to perform the common task of managing presets.
This example shows how you can add a preset menu.
This example uses the object display options,
however you can use properties defined by your own scripts too.
"""
import bpy
from bpy.types import Operator, Menu
from bl_operators.presets import AddPresetBase
class OBJECT_MT_display_presets(Menu):
bl_label = "Object Display Presets"
preset_subdir = "object/display"
preset_operator = "script.execute_preset"
draw = Menu.draw_preset
class AddPresetObjectDisplay(AddPresetBase, Operator):
'''Add a Object Display Preset'''
bl_idname = "camera.object_display_preset_add"
bl_label = "Add Object Display Preset"
preset_menu = "OBJECT_MT_display_presets"
# Variable used for all preset values.
preset_defines = [
"obj = bpy.context.object"
]
# Properties to store in the preset.
preset_values = [
"obj.display_type",
"obj.show_bounds",
"obj.display_bounds_type",
"obj.show_name",
"obj.show_axis",
"obj.show_wire",
]
# Where to store the preset.
preset_subdir = "object/display"
# Display into an existing panel.
def panel_func(self, context):
layout = self.layout
row = layout.row(align=True)
row.menu(OBJECT_MT_display_presets.__name__, text=OBJECT_MT_display_presets.bl_label)
row.operator(AddPresetObjectDisplay.bl_idname, text="", icon='ZOOM_IN')
row.operator(AddPresetObjectDisplay.bl_idname, text="", icon='ZOOM_OUT').remove_active = True
classes = (
OBJECT_MT_display_presets,
AddPresetObjectDisplay,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.OBJECT_PT_display.prepend(panel_func)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
bpy.types.OBJECT_PT_display.remove(panel_func)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,66 @@
"""
Extending the Button Context Menu
+++++++++++++++++++++++++++++++++
This example enables you to insert your own menu entry into the common
right click menu that you get while hovering over a UI button (e.g. operator,
value field, color, string, etc.)
To make the example work, you have to first select an object
then right click on an user interface element (maybe a color in the
material properties) and choose *Execute Custom Action*.
Executing the operator will then print all values.
"""
import bpy
def dump(obj, text):
for attr in dir(obj):
print("{!r}.{:s} = {!s}".format(obj, attr, getattr(obj, attr)))
class WM_OT_button_context_test(bpy.types.Operator):
"""Right click entry test"""
bl_idname = "wm.button_context_test"
bl_label = "Run Context Test"
@classmethod
def poll(cls, context):
return context.active_object is not None
def execute(self, context):
value = getattr(context, "button_pointer", None)
if value is not None:
dump(value, "button_pointer")
value = getattr(context, "button_prop", None)
if value is not None:
dump(value, "button_prop")
value = getattr(context, "button_operator", None)
if value is not None:
dump(value, "button_operator")
return {'FINISHED'}
def draw_menu(self, context):
layout = self.layout
layout.separator()
layout.operator(WM_OT_button_context_test.bl_idname)
def register():
bpy.utils.register_class(WM_OT_button_context_test)
bpy.types.UI_MT_button_context_menu.append(draw_menu)
def unregister():
bpy.types.UI_MT_button_context_menu.remove(draw_menu)
bpy.utils.unregister_class(WM_OT_button_context_test)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,41 @@
"""
Mesh Data
+++++++++
The mesh data is accessed in object mode and intended for compact storage,
for more flexible mesh editing from Python see :mod:`bmesh`.
Blender stores 4 main arrays to define mesh geometry.
- :class:`Mesh.vertices` (3 points in space)
- :class:`Mesh.edges` (reference 2 vertices)
- :class:`Mesh.loops` (reference a single vertex and edge)
- :class:`Mesh.polygons`: (reference a range of loops)
Each polygon references a slice in the loop array, this way,
polygons do not store vertices or corner data such as UVs directly,
only a reference to loops that the polygon uses.
:class:`Mesh.loops`, :class:`Mesh.uv_layers` :class:`Mesh.vertex_colors` are all aligned so the same polygon loop
indices can be used to find the UVs and vertex colors as with as the vertices.
To compare mesh API options see: :ref:`NGons and Tessellation Faces <info_gotcha_mesh_faces>`
This example script prints the vertices and UVs for each polygon, assumes the active object is a mesh with UVs.
"""
import bpy
me = bpy.context.object.data
uv_layer = me.uv_layers.active.data
for poly in me.polygons:
print("Polygon index: {:d}, length: {:d}".format(poly.index, poly.loop_total))
# Range is used here to show how the polygons reference loops,
# for convenience 'poly.loop_indices' can be used instead.
for loop_index in range(poly.loop_start, poly.loop_start + poly.loop_total):
print(" Vertex: {:d}".format(me.loops[loop_index].vertex_index))
print(" UV: {!r}".format(uv_layer[loop_index].uv))

View File

@@ -0,0 +1,26 @@
"""
Poll Function
+++++++++++++++
The :class:`NodeTree.poll` function determines if a node tree is visible
in the given context (similar to how :class:`Panel.poll`
and :class:`Menu.poll` define visibility). If it returns False,
the node tree type will not be selectable in the node editor.
A typical condition for shader nodes would be to check the active render engine
of the scene and only show nodes of the renderer they are designed for.
"""
import bpy
class CyclesNodeTree(bpy.types.NodeTree):
""" This operator is only visible when Cycles is the selected render engine"""
bl_label = "Cycles Node Tree"
bl_icon = 'NONE'
@classmethod
def poll(cls, context):
return context.scene.render.engine == 'CYCLES'
bpy.utils.register_class(CyclesNodeTree)

View File

@@ -0,0 +1,28 @@
"""
Basic Object Operations Example
+++++++++++++++++++++++++++++++
This script demonstrates basic operations on object like creating new
object, placing it into a view layer, selecting it and making it active.
"""
import bpy
view_layer = bpy.context.view_layer
# Create new light data-block.
light_data = bpy.data.lights.new(name="New Light", type='POINT')
# Create new object with our light data-block.
light_object = bpy.data.objects.new(name="New Light", object_data=light_data)
# Link light object to the active collection of current view layer,
# so that it'll appear in the current scene.
view_layer.active_layer_collection.collection.objects.link(light_object)
# Place light to a specified location.
light_object.location = (5.0, 5.0, 5.0)
# And finally select it and make it active.
light_object.select_set(True)
view_layer.objects.active = light_object

View File

@@ -0,0 +1,41 @@
"""
Basic Operator Example
++++++++++++++++++++++
This script shows simple operator which prints a message.
Since the operator only has an :class:`Operator.execute` function it takes no
user input.
The function should return ``{'FINISHED'}`` or ``{'CANCELLED'}``, the latter
meaning that operator execution was aborted without making any changes, and
that no undo step will created (see next example for more info about undo).
.. note::
Operator subclasses must be registered before accessing them from Blender.
"""
import bpy
class HelloWorldOperator(bpy.types.Operator):
bl_idname = "wm.hello_world"
bl_label = "Minimal Operator"
def execute(self, context):
print("Hello World")
return {'FINISHED'}
# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
self.layout.operator(HelloWorldOperator.bl_idname, text="Hello World Operator")
# Register and add to the view menu (required to also use F3 search "Hello World Operator" for quick access).
bpy.utils.register_class(HelloWorldOperator)
bpy.types.VIEW3D_MT_view.append(menu_func)
# Test call to the newly defined operator.
bpy.ops.wm.hello_world()

View File

@@ -0,0 +1,63 @@
"""
.. _operator_modifying_blender_data_undo:
Modifying Blender Data & Undo
+++++++++++++++++++++++++++++
Any operator modifying Blender data should enable the ``'UNDO'`` option.
This will make Blender automatically create an undo step when the operator
finishes its ``execute`` (or ``invoke``, see below) functions, and returns
``{'FINISHED'}``.
Otherwise, no undo step will be created, which will at best corrupt the
undo stack and confuse the user (since modifications done by the operator
may either not be undoable, or be undone together with other edits done
before). In many cases, this can even lead to data corruption and crashes.
Note that when an operator returns ``{'CANCELLED'}``, no undo step will be
created. This means that if an error occurs *after* modifying some data
already, it is better to return ``{'FINISHED'}``, unless it is possible to
fully undo the changes before returning.
.. note::
Most examples in this page do not do any edit to Blender data, which is
why it is safe to keep the default ``bl_options`` value for these operators.
.. note::
In some complex cases, the automatic undo step created on operator exit may
not be enough. For example, if the operator does mode switching, or calls
other operators that should create an extra undo step, etc.
Such manual undo push is possible using the :class:`bpy.ops.ed.undo_push`
function. Be careful though, this is considered an advanced feature and
requires some understanding of the actual undo system in Blender code.
"""
import bpy
class DataEditOperator(bpy.types.Operator):
bl_idname = "object.data_edit"
bl_label = "Data Editing Operator"
# The default value is only 'REGISTER', 'UNDO' is mandatory when Blender data is modified
# (and does require 'REGISTER' as well).
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
context.object.location.x += 1.0
return {'FINISHED'}
# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
self.layout.operator(DataEditOperator.bl_idname, text="Blender Data Editing Operator")
# Register.
bpy.utils.register_class(DataEditOperator)
bpy.types.VIEW3D_MT_view.append(menu_func)
# Test call to the newly defined operator.
bpy.ops.object.data_edit()

View File

@@ -0,0 +1,68 @@
"""
Invoke Function
+++++++++++++++
:class:`Operator.invoke` is used to initialize the operator from the context
at the moment the operator is called.
invoke() is typically used to assign properties which are then used by
execute().
Some operators don't have an execute() function, removing the ability to be
repeated from a script or macro.
When an operator is called via :mod:`bpy.ops`, the execution context depends
on the argument provided to :mod:`bpy.ops`. By default, it uses execute().
When an operator is activated from a button or menu item, it follows
the setting in :class:`UILayout.operator_context`. In most cases, invoke() is used.
Running an operator via a key shortcut always uses invoke(),
and this behavior cannot be changed.
This example shows how to define an operator which gets mouse input to
execute a function and that this operator can be invoked or executed from
the Python API.
Also notice this operator defines its own properties, these are different
to typical class properties because Blender registers them with the
operator, to use as arguments when called, saved for operator undo/redo and
automatically added into the user interface.
"""
import bpy
class SimpleMouseOperator(bpy.types.Operator):
""" This operator shows the mouse location,
this string is used for the tooltip and API docs
"""
bl_idname = "wm.mouse_position"
bl_label = "Invoke Mouse Operator"
x: bpy.props.IntProperty()
y: bpy.props.IntProperty()
def execute(self, context):
# Rather than printing, use the report function,
# this way the message appears in the header.
self.report({'INFO'}, "Mouse coords are {:d} {:d}".format(self.x, self.y))
return {'FINISHED'}
def invoke(self, context, event):
self.x = event.mouse_x
self.y = event.mouse_y
return self.execute(context)
# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
self.layout.operator(SimpleMouseOperator.bl_idname, text="Simple Mouse Operator")
# Register and add to the view menu (required to also use F3 search "Simple Mouse Operator" for quick access).
bpy.utils.register_class(SimpleMouseOperator)
bpy.types.VIEW3D_MT_view.append(menu_func)
# Test call to the newly defined operator.
# Here we call the operator and invoke it,
# meaning that the settings are taken from the mouse.
bpy.ops.wm.mouse_position('INVOKE_DEFAULT')
# Another test call, this time call execute() directly with pre-defined settings.
bpy.ops.wm.mouse_position('EXEC_DEFAULT', x=20, y=66)

View File

@@ -0,0 +1,52 @@
"""
Calling a File Selector
+++++++++++++++++++++++
This example shows how an operator can use the file selector.
Notice the invoke function calls a window manager method and returns
``{'RUNNING_MODAL'}``, this means the file selector stays open and the operator does not
exit immediately after invoke finishes.
The file selector runs the operator, calling :class:`Operator.execute` when the
user confirms.
The :class:`Operator.poll` function is optional, used to check if the operator
can run.
"""
import bpy
class ExportSomeData(bpy.types.Operator):
"""Test exporter which just writes hello world"""
bl_idname = "export.some_data"
bl_label = "Export Some Data"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
@classmethod
def poll(cls, context):
return context.object is not None
def execute(self, context):
file = open(self.filepath, 'w')
file.write("Hello World " + context.object.name)
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
self.layout.operator_context = 'INVOKE_DEFAULT'
self.layout.operator(ExportSomeData.bl_idname, text="Text Export Operator")
# Register and add to the file selector (required to also use F3 search "Text Export Operator" for quick access).
bpy.utils.register_class(ExportSomeData)
bpy.types.TOPBAR_MT_file_export.append(menu_func)
# Test call.
bpy.ops.export.some_data('INVOKE_DEFAULT')

View File

@@ -0,0 +1,40 @@
"""
Dialog Box
++++++++++
This operator uses its :class:`Operator.invoke` function to call a popup.
"""
import bpy
class DialogOperator(bpy.types.Operator):
bl_idname = "object.dialog_operator"
bl_label = "Simple Dialog Operator"
my_float: bpy.props.FloatProperty(name="Some Floating Point")
my_bool: bpy.props.BoolProperty(name="Toggle Option")
my_string: bpy.props.StringProperty(name="String Value")
def execute(self, context):
message = "Popup Values: {:f}, {:d}, '{:s}'".format(
self.my_float, self.my_bool, self.my_string,
)
self.report({'INFO'}, message)
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
self.layout.operator(DialogOperator.bl_idname, text="Dialog Operator")
# Register and add to the object menu (required to also use F3 search "Dialog Operator" for quick access).
bpy.utils.register_class(DialogOperator)
bpy.types.VIEW3D_MT_object.append(menu_func)
# Test call.
bpy.ops.object.dialog_operator('INVOKE_DEFAULT')

View File

@@ -0,0 +1,55 @@
"""
Custom Drawing
++++++++++++++
By default operator properties use an automatic user interface layout.
If you need more control you can create your own layout with a
:class:`Operator.draw` function.
This works like the :class:`Panel` and :class:`Menu` draw functions, its used
for dialogs and file selectors.
"""
import bpy
class CustomDrawOperator(bpy.types.Operator):
bl_idname = "object.custom_draw"
bl_label = "Simple Modal Operator"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
my_float: bpy.props.FloatProperty(name="Float")
my_bool: bpy.props.BoolProperty(name="Toggle Option")
my_string: bpy.props.StringProperty(name="String Value")
def execute(self, context):
print("Test", self)
return {'FINISHED'}
def invoke(self, context, event):
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
col = layout.column()
col.label(text="Custom Interface!")
row = col.row()
row.prop(self, "my_float")
row.prop(self, "my_bool")
col.prop(self, "my_string")
# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
self.layout.operator(CustomDrawOperator.bl_idname, text="Custom Draw Operator")
# Register and add to the object menu (required to also use F3 search "Custom Draw Operator" for quick access).
bpy.utils.register_class(CustomDrawOperator)
bpy.types.VIEW3D_MT_object.append(menu_func)
# Test call.
bpy.ops.object.custom_draw('INVOKE_DEFAULT')

View File

@@ -0,0 +1,77 @@
"""
.. _modal_operator:
Modal Execution
+++++++++++++++
This operator defines a :class:`Operator.modal` function that will keep being
run to handle events until it returns ``{'FINISHED'}`` or ``{'CANCELLED'}``.
Modal operators run every time a new event is detected, such as a mouse click
or key press. Conversely, when no new events are detected, the modal operator
will not run. Modal operators are especially useful for interactive tools, an
operator can have its own state where keys toggle options as the operator runs.
Grab, Rotate, Scale, and Fly-Mode are examples of modal operators.
:class:`Operator.invoke` is used to initialize the operator as being active
by returning ``{'RUNNING_MODAL'}``, initializing the modal loop.
Notice ``__init__()`` and ``__del__()`` are declared.
For other operator types they are not useful but for modal operators they will
be called before the :class:`Operator.invoke` and after the operator finishes.
Also see the
:ref:`class construction and destruction section <info_overview_class_construction_destruction>`.
"""
import bpy
class ModalOperator(bpy.types.Operator):
bl_idname = "object.modal_operator"
bl_label = "Simple Modal Operator"
bl_options = {'REGISTER', 'UNDO'}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
print("Start")
def __del__(self):
print("End")
super().__del__()
def execute(self, context):
context.object.location.x = self.value / 100.0
return {'FINISHED'}
def modal(self, context, event):
if event.type == 'MOUSEMOVE': # Apply.
self.value = event.mouse_x
self.execute(context)
elif event.type == 'LEFTMOUSE': # Confirm.
return {'FINISHED'}
elif event.type in {'RIGHTMOUSE', 'ESC'}: # Cancel.
# Revert all changes that have been made
context.object.location.x = self.init_loc_x
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
self.init_loc_x = context.object.location.x
self.value = event.mouse_x
self.execute(context)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
self.layout.operator(ModalOperator.bl_idname, text="Modal Operator")
# Register and add to the object menu (required to also use F3 search "Modal Operator" for quick access).
bpy.utils.register_class(ModalOperator)
bpy.types.VIEW3D_MT_object.append(menu_func)
# Test call.
bpy.ops.object.modal_operator('INVOKE_DEFAULT')

View File

@@ -0,0 +1,45 @@
"""
Enum Search Popup
+++++++++++++++++
You may want to have an operator prompt the user to select an item
from a search field, this can be done using :class:`bpy.types.Operator.invoke_search_popup`.
"""
import bpy
from bpy.props import EnumProperty
class SearchEnumOperator(bpy.types.Operator):
bl_idname = "object.search_enum_operator"
bl_label = "Search Enum Operator"
bl_property = "my_search"
my_search: EnumProperty(
name="My Search",
items=(
('FOO', "Foo", ""),
('BAR', "Bar", ""),
('BAZ', "Baz", ""),
),
)
def execute(self, context):
self.report({'INFO'}, "Selected:" + self.my_search)
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {'RUNNING_MODAL'}
# Only needed if you want to add into a dynamic menu.
def menu_func(self, context):
self.layout.operator(SearchEnumOperator.bl_idname, text="Search Enum Operator")
# Register and add to the object menu (required to also use F3 search "Search Enum Operator" for quick access).
bpy.utils.register_class(SearchEnumOperator)
bpy.types.VIEW3D_MT_object.append(menu_func)
# Test call.
bpy.ops.object.search_enum_operator('INVOKE_DEFAULT')

View File

@@ -0,0 +1,29 @@
"""
Basic Panel Example
+++++++++++++++++++
This script is a simple panel which will draw into the object properties
section.
Notice the 'CATEGORY_PT_name' :class:`Panel.bl_idname`, this is a naming
convention for panels.
.. note::
Panel subclasses must be registered for Blender to use them.
"""
import bpy
class HelloWorldPanel(bpy.types.Panel):
bl_idname = "OBJECT_PT_hello_world"
bl_label = "Hello World"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
self.layout.label(text="Hello World")
bpy.utils.register_class(HelloWorldPanel)

View File

@@ -0,0 +1,38 @@
"""
Simple Object Panel
+++++++++++++++++++
This panel has a :class:`Panel.poll` and :class:`Panel.draw_header` function,
even though the contents is basic this closely resembles blenders panels.
"""
import bpy
class ObjectSelectPanel(bpy.types.Panel):
bl_idname = "OBJECT_PT_select"
bl_label = "Select"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
bl_options = {'DEFAULT_CLOSED'}
@classmethod
def poll(cls, context):
return (context.object is not None)
def draw_header(self, context):
layout = self.layout
layout.label(text="My Select Panel")
def draw(self, context):
layout = self.layout
box = layout.box()
box.label(text="Selection Tools")
box.operator("object.select_all").action = 'TOGGLE'
row = box.row()
row.operator("object.select_all").action = 'INVERT'
row.operator("object.select_random")
bpy.utils.register_class(ObjectSelectPanel)

View File

@@ -0,0 +1,37 @@
"""
Mix-in Classes
++++++++++++++
A mix-in parent class can be used to share common properties and
:class:`Menu.poll` function.
"""
import bpy
class View3DPanel:
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Tool"
@classmethod
def poll(cls, context):
return (context.object is not None)
class PanelOne(View3DPanel, bpy.types.Panel):
bl_idname = "VIEW3D_PT_test_1"
bl_label = "Panel One"
def draw(self, context):
self.layout.label(text="Small Class")
class PanelTwo(View3DPanel, bpy.types.Panel):
bl_idname = "VIEW3D_PT_test_2"
bl_label = "Panel Two"
def draw(self, context):
self.layout.label(text="Also Small Class")
bpy.utils.register_class(PanelOne)
bpy.utils.register_class(PanelTwo)

View File

@@ -0,0 +1,37 @@
"""
This example shows how to use B-Bone segment matrices to emulate deformation
produced by the Armature modifier or constraint when assigned to the given bone
(without Preserve Volume). The coordinates are processed in armature Pose space:
"""
import bpy
def bbone_deform_matrix(pose_bone, point):
index, blend_next = pose_bone.bbone_segment_index(point)
rest1 = pose_bone.bbone_segment_matrix(index, rest=True)
pose1 = pose_bone.bbone_segment_matrix(index, rest=False)
deform1 = pose1 @ rest1.inverted()
# `bbone_segment_index` ensures that index + 1 is always valid
rest2 = pose_bone.bbone_segment_matrix(index + 1, rest=True)
pose2 = pose_bone.bbone_segment_matrix(index + 1, rest=False)
deform2 = pose2 @ rest2.inverted()
deform = deform1 * (1 - blend_next) + deform2 * blend_next
return pose_bone.matrix @ deform @ pose_bone.bone.matrix_local.inverted()
# Armature modifier deforming vertices:
mesh = bpy.data.objects["Mesh"]
pose_bone = bpy.data.objects["Armature"].pose.bones["Bone"]
for vertex in mesh.data.vertices:
vertex.co = bbone_deform_matrix(pose_bone, vertex.co) @ vertex.co
# Armature constraint modifying an object transform:
empty = bpy.data.objects["Empty"]
matrix = empty.matrix_world
empty.matrix_world = bbone_deform_matrix(pose_bone, matrix.translation) @ matrix

View File

@@ -0,0 +1,41 @@
"""
Custom Properties
+++++++++++++++++
PropertyGroups are the base class for dynamically defined sets of properties.
They can be used to extend existing Blender data with your own types which can
be animated, accessed from the user interface and from Python.
.. note::
The values assigned to Blender data are saved to disk but the class
definitions are not, this means whenever you load Blender the class needs
to be registered too.
This is best done by creating an add-on which loads on startup and registers
your properties.
.. note::
PropertyGroups must be registered before assigning them to Blender data.
.. seealso::
Property types used in class declarations are all in :mod:`bpy.props`
"""
import bpy
class MyPropertyGroup(bpy.types.PropertyGroup):
custom_1: bpy.props.FloatProperty(name="My Float")
custom_2: bpy.props.IntProperty(name="My Int")
bpy.utils.register_class(MyPropertyGroup)
bpy.types.Object.my_prop_grp = bpy.props.PointerProperty(type=MyPropertyGroup)
# Test this worked.
bpy.data.objects[0].my_prop_grp.custom_1 = 22.0

View File

@@ -0,0 +1,186 @@
"""
Simple Render Engine
++++++++++++++++++++
"""
import bpy
import array
class CustomRenderEngine(bpy.types.RenderEngine):
# These three members are used by Blender to set up the
# RenderEngine; define its internal name, visible name and capabilities.
bl_idname = "CUSTOM"
bl_label = "Custom"
bl_use_preview = True
# Init is called whenever a new render engine instance is created. Multiple
# instances may exist at the same time, for example for a viewport and final
# render.
# Note the generic arguments signature, and the call to the parent class
# `__init__` methods, which are required for Blender to create the underlying
# `RenderEngine` data.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.scene_data = None
self.draw_data = None
# When the render engine instance is destroy, this is called. Clean up any
# render engine data here, for example stopping running render threads.
def __del__(self):
# Own delete code...
super().__del__()
# This is the method called by Blender for both final renders (F12) and
# small preview for materials, world and lights.
def render(self, depsgraph):
scene = depsgraph.scene
scale = scene.render.resolution_percentage / 100.0
self.size_x = int(scene.render.resolution_x * scale)
self.size_y = int(scene.render.resolution_y * scale)
# Fill the render result with a flat color. The frame-buffer is
# defined as a list of pixels, each pixel itself being a list of
# R,G,B,A values.
if self.is_preview:
color = [0.1, 0.2, 0.1, 1.0]
else:
color = [0.2, 0.1, 0.1, 1.0]
pixel_count = self.size_x * self.size_y
rect = [color] * pixel_count
# Here we write the pixel values to the RenderResult
result = self.begin_result(0, 0, self.size_x, self.size_y)
layer = result.layers[0].passes["Combined"]
layer.rect = rect
self.end_result(result)
# For viewport renders, this method gets called once at the start and
# whenever the scene or 3D viewport changes. This method is where data
# should be read from Blender in the same thread. Typically a render
# thread will be started to do the work while keeping Blender responsive.
def view_update(self, context, depsgraph):
region = context.region
view3d = context.space_data
scene = depsgraph.scene
# Get viewport dimensions
dimensions = region.width, region.height
if not self.scene_data:
# First time initialization
self.scene_data = []
first_time = True
# Loop over all datablocks used in the scene.
for datablock in depsgraph.ids:
pass
else:
first_time = False
# Test which datablocks changed
for update in depsgraph.updates:
print("Datablock updated: ", update.id.name)
# Test if any material was added, removed or changed.
if depsgraph.id_type_updated('MATERIAL'):
print("Materials updated")
# Loop over all object instances in the scene.
if first_time or depsgraph.id_type_updated('OBJECT'):
for instance in depsgraph.object_instances:
pass
# For viewport renders, this method is called whenever Blender redraws
# the 3D viewport. The renderer is expected to quickly draw the render
# with OpenGL, and not perform other expensive work.
# Blender will draw overlays for selection and editing on top of the
# rendered image automatically.
def view_draw(self, context, depsgraph):
# Lazily import GPU module, so that the render engine works in
# background mode where the GPU module can't be imported by default.
import gpu
region = context.region
scene = depsgraph.scene
# Get viewport dimensions
dimensions = region.width, region.height
# Bind shader that converts from scene linear to display space,
gpu.state.blend_set('ALPHA_PREMULT')
self.bind_display_space_shader(scene)
if not self.draw_data or self.draw_data.dimensions != dimensions:
self.draw_data = CustomDrawData(dimensions)
self.draw_data.draw()
self.unbind_display_space_shader()
gpu.state.blend_set('NONE')
class CustomDrawData:
def __init__(self, dimensions):
import gpu
# Generate dummy float image buffer.
self.dimensions = dimensions
width, height = dimensions
pixels = width * height * array.array('f', [0.1, 0.2, 0.1, 1.0])
pixels = gpu.types.Buffer('FLOAT', width * height * 4, pixels)
# Generate texture.
self.texture = gpu.types.GPUTexture((width, height), format='RGBA16F', data=pixels)
# Note: This is just a didactic example.
# In this case it would be more convenient to fill the texture with:
# self.texture.clear('FLOAT', value=[0.1, 0.2, 0.1, 1.0])
def __del__(self):
del self.texture
def draw(self):
from gpu_extras.presets import draw_texture_2d
draw_texture_2d(self.texture, (0, 0), self.texture.width, self.texture.height)
# RenderEngines also need to tell UI Panels that they are compatible with.
# We recommend to enable all panels marked as BLENDER_RENDER, and then
# exclude any panels that are replaced by custom panels registered by the
# render engine, or that are not supported.
def get_panels():
exclude_panels = {
'VIEWLAYER_PT_filter',
'VIEWLAYER_PT_layer_passes',
}
panels = []
for panel in bpy.types.Panel.__subclasses__():
if hasattr(panel, 'COMPAT_ENGINES') and 'BLENDER_RENDER' in panel.COMPAT_ENGINES:
if panel.__name__ not in exclude_panels:
panels.append(panel)
return panels
def register():
# Register the RenderEngine.
bpy.utils.register_class(CustomRenderEngine)
for panel in get_panels():
panel.COMPAT_ENGINES.add('CUSTOM')
def unregister():
bpy.utils.unregister_class(CustomRenderEngine)
for panel in get_panels():
if 'CUSTOM' in panel.COMPAT_ENGINES:
panel.COMPAT_ENGINES.remove('CUSTOM')
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,36 @@
"""
GPU Render Engine
+++++++++++++++++
"""
import bpy
class CustomGPURenderEngine(bpy.types.RenderEngine):
bl_idname = "CUSTOM_GPU"
bl_label = "Custom GPU"
# Request a GPU context to be created and activated for the render method.
# This may be used either to perform the rendering itself, or to allocate
# and fill a texture for more efficient drawing.
bl_use_gpu_context = True
def render(self, depsgraph):
# Lazily import GPU module, since GPU context is only created on demand
# for rendering and does not exist on register.
import gpu
# Perform rendering task.
pass
def register():
bpy.utils.register_class(CustomGPURenderEngine)
def unregister():
bpy.utils.unregister_class(CustomGPURenderEngine)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,79 @@
"""
Basic UIList Example
++++++++++++++++++++
This script is the UIList subclass used to show material slots, with a bunch of additional commentaries.
Notice the name of the class, this naming convention is similar as the one for panels or menus.
.. note::
UIList subclasses must be registered for Blender to use them.
"""
import bpy
class MATERIAL_UL_matslots_example(bpy.types.UIList):
# The draw_item function is called for each item of the collection that is visible in the list.
# data is the RNA object containing the collection,
# item is the current drawn item of the collection,
# icon is the "computed" icon for the item (as an integer, because some objects like materials or textures
# have custom icons ID, which are not available as enum items).
# active_data is the RNA object containing the active property for the collection (i.e. integer pointing to the
# active item of the collection).
# active_propname is the name of the active property (use 'getattr(active_data, active_propname)').
# index is index of the current item in the collection.
# flt_flag is the result of the filtering process for this item.
# Note: as index and flt_flag are optional arguments, you do not have to use/declare them here if you don't
# need them.
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
ob = data
slot = item
ma = slot.material
# You should always start your row layout by a label (icon + text), or a non-embossed text field,
# this will also make the row easily selectable in the list! The later also enables ctrl-click rename.
# We use icon_value of label, as our given icon is an integer value, not an enum ID.
# Note "data" names should never be translated!
if ma:
layout.prop(ma, "name", text="", emboss=False, icon_value=icon)
else:
layout.label(text="", translate=False, icon_value=icon)
# And now we can use this list everywhere in Blender. Here is a small example panel.
class UIListPanelExample1(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
bl_label = "UIList Example 1 Panel"
bl_idname = "OBJECT_PT_ui_list_example_1"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
obj = context.object
# `template_list` now takes two new arguments.
# The first one is the identifier of the registered UIList to use (if you want only the default list,
# with no custom draw code, use "UI_UL_list").
layout.template_list("MATERIAL_UL_matslots_example", "", obj, "material_slots", obj, "active_material_index")
# The second one can usually be left as an empty string.
# It's an additional ID used to distinguish lists in case you use the same list several times in a given area.
layout.template_list("MATERIAL_UL_matslots_example", "compact", obj, "material_slots",
obj, "active_material_index", type='COMPACT')
def register():
bpy.utils.register_class(MATERIAL_UL_matslots_example)
bpy.utils.register_class(UIListPanelExample1)
def unregister():
bpy.utils.unregister_class(UIListPanelExample1)
bpy.utils.unregister_class(MATERIAL_UL_matslots_example)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,216 @@
"""
Advanced UIList Example - Filtering and Reordering
++++++++++++++++++++++++++++++++++++++++++++++++++
This script is an extended version of the ``UIList`` subclass used to show vertex groups. It is not used 'as is',
because iterating over all vertices in a 'draw' function is a very bad idea for UI performance! However, it's a good
example of how to create/use filtering/reordering callbacks.
"""
import bpy
class MESH_UL_vgroups_slow(bpy.types.UIList):
# Constants (flags).
# Be careful not to shadow FILTER_ITEM!
VGROUP_EMPTY = 1 << 0
# Custom properties, saved with `.blend` file.
use_filter_empty: bpy.props.BoolProperty(
name="Filter Empty",
default=False,
options=set(),
description="Whether to filter empty vertex groups",
)
use_filter_empty_reverse: bpy.props.BoolProperty(
name="Reverse Empty",
default=False,
options=set(),
description="Reverse empty filtering",
)
use_filter_name_reverse: bpy.props.BoolProperty(
name="Reverse Name",
default=False,
options=set(),
description="Reverse name filtering",
)
use_filter_orderby_invert: bpy.props.BoolProperty(
name="Reverse Order",
default=False,
options=set(),
description="Reverse order filtering",
)
# This allows us to have mutually exclusive options, which are also all disable-able!
def _gen_order_update(name1, name2):
def _u(self, ctxt):
if (getattr(self, name1)):
setattr(self, name2, False)
return _u
use_order_name: bpy.props.BoolProperty(
name="Name", default=False, options=set(),
description="Sort groups by their name (case-insensitive)",
update=_gen_order_update("use_order_name", "use_order_importance"),
)
use_order_importance: bpy.props.BoolProperty(
name="Importance",
default=False,
options=set(),
description="Sort groups by their average weight in the mesh",
update=_gen_order_update("use_order_importance", "use_order_name"),
)
# Usual draw item function.
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, flt_flag):
# Just in case, we do not use it here!
self.use_filter_invert = False
# assert(isinstance(item, bpy.types.VertexGroup)
vgroup = item
# Here we use one feature of new filtering feature: it can pass data to draw_item, through flt_flag
# parameter, which contains exactly what filter_items set in its filter list for this item!
# In this case, we show empty groups grayed out.
if flt_flag & self.VGROUP_EMPTY:
col = layout.column()
col.enabled = False
col.alignment = 'LEFT'
col.prop(vgroup, "name", text="", emboss=False, icon_value=icon)
else:
layout.prop(vgroup, "name", text="", emboss=False, icon_value=icon)
icon = 'LOCKED' if vgroup.lock_weight else 'UNLOCKED'
layout.prop(vgroup, "lock_weight", text="", icon=icon, emboss=False)
def draw_filter(self, context, layout):
# Nothing much to say here, it's usual UI code...
row = layout.row()
subrow = row.row(align=True)
subrow.prop(self, "filter_name", text="")
icon = 'ZOOM_OUT' if self.use_filter_name_reverse else 'ZOOM_IN'
subrow.prop(self, "use_filter_name_reverse", text="", icon=icon)
subrow = row.row(align=True)
subrow.prop(self, "use_filter_empty", toggle=True)
icon = 'ZOOM_OUT' if self.use_filter_empty_reverse else 'ZOOM_IN'
subrow.prop(self, "use_filter_empty_reverse", text="", icon=icon)
row = layout.row(align=True)
row.label(text="Order by:")
row.prop(self, "use_order_name", toggle=True)
row.prop(self, "use_order_importance", toggle=True)
icon = 'TRIA_UP' if self.use_filter_orderby_invert else 'TRIA_DOWN'
row.prop(self, "use_filter_orderby_invert", text="", icon=icon)
def filter_items_empty_vgroups(self, context, vgroups):
# This helper function checks vgroups to find out whether they are empty, and what's their average weights.
# TODO: This should be RNA helper actually (a vgroup prop like `"raw_data: ((vidx, vweight), etc.)"`).
# Too slow for Python!
obj_data = context.active_object.data
ret = {vg.index: [True, 0.0] for vg in vgroups}
if hasattr(obj_data, "vertices"): # Mesh data
if obj_data.is_editmode:
import bmesh
bm = bmesh.from_edit_mesh(obj_data)
# only ever one deform weight layer
dvert_lay = bm.verts.layers.deform.active
fact = 1 / len(bm.verts)
if dvert_lay:
for v in bm.verts:
for vg_idx, vg_weight in v[dvert_lay].items():
ret[vg_idx][0] = False
ret[vg_idx][1] += vg_weight * fact
else:
fact = 1 / len(obj_data.vertices)
for v in obj_data.vertices:
for vg in v.groups:
ret[vg.group][0] = False
ret[vg.group][1] += vg.weight * fact
elif hasattr(obj_data, "points"): # Lattice data
# XXX: no access to lattice edit-data?
fact = 1 / len(obj_data.points)
for v in obj_data.points:
for vg in v.groups:
ret[vg.group][0] = False
ret[vg.group][1] += vg.weight * fact
return ret
def filter_items(self, context, data, propname):
# This function gets the collection property (as the usual tuple (data, propname)), and must return two lists:
# * The first one is for filtering, it must contain 32bit integers were self.bitflag_filter_item marks the
# matching item as filtered (i.e. to be shown). The upper 16 bits (including `self.bitflag_filter_item`) are
# reserved for internal use, the lower 16 bits are free for custom use. Here we use the first bit to mark
# VGROUP_EMPTY.
# * The second one is for reordering, it must return a list containing the new indices of the items (which
# gives us a mapping `org_idx -> new_idx`).
# Please note that the default UI_UL_list defines helper functions for common tasks (see its doc for more info).
# If you do not make filtering and/or ordering, return empty list(s) (this will be more efficient than
# returning full lists doing nothing!).
vgroups = getattr(data, propname)
helper_funcs = bpy.types.UI_UL_list
# Default return values.
flt_flags = []
flt_neworder = []
# Pre-compute of vertex-groups data, unfortunately this is CPU-intensive.
vgroups_empty = self.filter_items_empty_vgroups(context, vgroups)
# Filtering by name.
if self.filter_name:
flt_flags = helper_funcs.filter_items_by_name(self.filter_name, self.bitflag_filter_item, vgroups, "name",
reverse=self.use_filter_name_reverse)
if not flt_flags:
flt_flags = [self.bitflag_filter_item] * len(vgroups)
# Filter by emptiness.
for idx, vg in enumerate(vgroups):
if vgroups_empty[vg.index][0]:
flt_flags[idx] |= self.VGROUP_EMPTY
if self.use_filter_empty and self.use_filter_empty_reverse:
flt_flags[idx] &= ~self.bitflag_filter_item
elif self.use_filter_empty and not self.use_filter_empty_reverse:
flt_flags[idx] &= ~self.bitflag_filter_item
# Reorder by name or average weight.
if self.use_order_name:
flt_neworder = helper_funcs.sort_items_by_name(vgroups, "name")
if self.use_filter_orderby_invert:
flt_neworder.reverse()
elif self.use_order_importance:
_sort = [(idx, vgroups_empty[vg.index][1]) for idx, vg in enumerate(vgroups)]
highest_first = not self.use_filter_orderby_invert
flt_neworder = helper_funcs.sort_items_helper(_sort, lambda e: e[1], highest_first)
return flt_flags, flt_neworder
# Minimal code to use above UIList...
class UIListPanelExample2(bpy.types.Panel):
"""Creates a Panel in the Object properties window"""
bl_label = "UIList Example 2 Panel"
bl_idname = "OBJECT_PT_ui_list_example_2"
bl_space_type = 'PROPERTIES'
bl_region_type = 'WINDOW'
bl_context = "object"
def draw(self, context):
layout = self.layout
obj = context.object
# `template_list` now takes two new arguments.
# The first one is the identifier of the registered UIList to use (if you want only the default list,
# with no custom draw code, use "UI_UL_list").
layout.template_list("MESH_UL_vgroups_slow", "", obj, "vertex_groups", obj.vertex_groups, "active_index")
def register():
bpy.utils.register_class(MESH_UL_vgroups_slow)
bpy.utils.register_class(UIListPanelExample2)
def unregister():
bpy.utils.unregister_class(UIListPanelExample2)
bpy.utils.unregister_class(MESH_UL_vgroups_slow)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,373 @@
"""
USD Hook Example
++++++++++++++++
This example shows an implementation of ``USDHook`` to extend USD
export and import functionality.
Callback Function API
---------------------
One may optionally define any or all of the following callback functions
in the ``USDHook`` subclass.
on_export
^^^^^^^^^
Called before the USD export finalizes, allowing modifications to the USD
stage immediately before it is saved.
Args:
- ``export_context`` (`USDSceneExportContext`_): Provides access to the stage and dependency graph
Returns:
- ``True`` on success or ``False`` if the operation was bypassed or otherwise failed to complete
on_material_export
^^^^^^^^^^^^^^^^^^
Called for each material that is exported, allowing modifications to the USD material,
such as shader generation.
Args:
- ``export_context`` (`USDMaterialExportContext`_): Provides access to the stage and a texture export utility function
- ``bl_material`` (``bpy.types.Material``): The source Blender material
- ``usd_material`` (``pxr.UsdShade.Material``): The target USD material to be exported
Returns:
- ``True`` on success or ``False`` if the operation was bypassed or otherwise failed to complete
Note that the target USD material might already have connected shaders created by the USD exporter or
by other material export hooks.
on_import
^^^^^^^^^
Called after the USD import finalizes.
Args:
- ``import_context`` (`USDSceneImportContext`_):
Provides access to the stage and a map associating USD prim paths and Blender IDs
Returns:
- ``True`` on success or ``False`` if the operation was bypassed or otherwise failed to complete
material_import_poll
^^^^^^^^^^^^^^^^^^^^
Called to determine if the ``USDHook`` implementation can convert a given USD material.
Args:
- ``import_context`` (`USDMaterialImportContext`_): Provides access to the stage and a texture import utility function
- ``usd_material`` (``pxr.UsdShade.Material``): The source USD material to be exported
Returns:
- ``True`` if the hook can convert the material or ``False`` otherwise
If any hook returns ``True`` from ``material_import_poll``, the USD importer will skip standard ``USD Preview Surface``
or ``MaterialX`` import and invoke the hook's `on_material_import`_ method to convert the material instead.
on_material_import
^^^^^^^^^^^^^^^^^^
Called for each material that is imported, to allow converting the USD material to nodes on the Blender material.
To ensure that this function gets called, the hook must also implement the ``material_import_poll()``
callback to return ``True`` for the given USD material.
Args:
- ``import_context`` (`USDMaterialImportContext`_): Provides access to the stage and a texture import utility function
- ``bl_material`` (``bpy.types.Material``): The target Blender material with an empty node tree
- ``usd_material`` (``pxr.UsdShade.Material``): The source USD material to be imported
Returns:
- ``True`` on success or ``False`` if the conversion failed or otherwise did not complete
Context Classes
---------------
Instances of the following built-in classes are provided as arguments to the callbacks.
USDSceneExportContext
^^^^^^^^^^^^^^^^^^^^^
Argument for `on_export`_.
Methods:
- ``get_stage()``: returns the USD stage to be saved
- ``get_depsgraph()``: returns the Blender scene dependency graph
- ``get_prim_map()`` returns a ``dict`` where the key is an exported USD Prim path and the value a ``list``
of the IDs associated with that prim.
USDMaterialExportContext
^^^^^^^^^^^^^^^^^^^^^^^^
Argument for `on_material_export`_.
Methods:
- ``get_stage()``: returns the USD stage to be saved
- ``export_texture(image: bpy.types.Image)``: Returns the USD asset path for the given texture image
The ``export_texture`` function will save in-memory images and may copy texture assets,
depending on the current USD export options.
For example, by default calling ``export_texture(/foo/bar.png)`` will copy the file to a ``textures``
directory next to the exported USD and will return the relative path ``./textures/bar.png``.
USDSceneImportContext
^^^^^^^^^^^^^^^^^^^^^
Argument for `on_import`_.
Methods:
- ``get_prim_map()`` returns a ``dict`` where the key is an imported USD Prim path and the value a ``list``
of the IDs created by the imported prim.
- ``get_stage()`` returns the USD stage which was imported.
USDMaterialImportContext
^^^^^^^^^^^^^^^^^^^^^^^^
Argument for `material_import_poll`_ and `on_material_import`_.
Methods:
- ``get_stage()``:
returns the USD stage to be saved.
- ``import_texture(asset_path: str)``:
for the given USD texture asset path, returns a ``tuple[str, bool]``,
containing the asset's local path and a bool indicating whether the path references a temporary file.
The ``import_texture`` function may copy the texture to the local file system if the given asset path is a
package-relative path for a USDZ archive, depending on the current USD ``Import Textures`` options.
When the ``Import Textures`` mode is ``Packed``, the texture is saved to a temporary location and the
second element of the returned tuple is ``True``, indicating that the file is temporary, in which
case it may be necessary to pack the image. The original asset path will be returned unchanged if it's
already a local file or if it could not be copied to a local destination.
Errors
------
Exceptions raised by these functions will be reported in Blender with the exception details printed to the console.
Example Code
------------
The ``USDHookExample`` class in the example below implements the following functions:
- ``on_export()`` function to add custom data to the stage's root layer.
- ``on_material_export()`` function to create a simple ``MaterialX`` shader on the given USD material.
- ``on_import()`` function to create a text object to display the stage's custom layer data.
- ``material_import_poll()`` returns ``True`` if the given USD material has an ``mtlx`` context.
- ``on_material_import()`` function to convert a simple ``MaterialX`` shader with a ``base_color`` input.
"""
bl_info = {
"name": "USD Hook Example",
"blender": (4, 4, 0),
}
import bpy
import bpy.types
import textwrap
# Make `pxr` module available, for running as `bpy` PIP package.
bpy.utils.expose_bundled_modules()
import pxr.Gf as Gf
import pxr.Sdf as Sdf
import pxr.Usd as Usd
import pxr.UsdShade as UsdShade
class USDHookExample(bpy.types.USDHook):
"""Example implementation of USD IO hooks"""
bl_idname = "usd_hook_example"
bl_label = "Example"
@staticmethod
def on_export(export_context):
""" Include the Blender filepath in the root layer custom data.
"""
stage = export_context.get_stage()
if stage is None:
return False
data = bpy.data
if data is None:
return False
# Set the custom data.
rootLayer = stage.GetRootLayer()
customData = rootLayer.customLayerData
customData["blenderFilepath"] = data.filepath
rootLayer.customLayerData = customData
return True
@staticmethod
def on_material_export(export_context, bl_material, usd_material):
""" Create a simple MaterialX shader on the exported material.
"""
stage = export_context.get_stage()
# Create a MaterialX standard surface shader
mtl_path = usd_material.GetPrim().GetPath()
shader = UsdShade.Shader.Define(stage, mtl_path.AppendPath("mtlxstandard_surface"))
shader.CreateIdAttr("ND_standard_surface_surfaceshader")
# Connect the shader. MaterialX materials use "mtlx" renderContext
usd_material.CreateSurfaceOutput("mtlx").ConnectToSource(shader.ConnectableAPI(), "out")
# Set the color to the Blender material's viewport display color.
col = bl_material.diffuse_color
shader.CreateInput("base_color", Sdf.ValueTypeNames.Color3f).Set(Gf.Vec3f(col[0], col[1], col[2]))
return True
@staticmethod
def on_import(import_context):
"""Inspect the imported stage & objects to set some custom data
"""
###########################################################
# Store some USD metadata on each imported data-block.
###########################################################
prim_map = import_context.get_prim_map()
# Store prim path as a string on each data-block created.
for prim_path, data_blocks in prim_map.items():
# Type hints for prim map.
prim_path: Sdf.Path
data_blocks: list[bpy.types.ID]
# Loop over mapped data-blocks to store some metadata.
for data_block in data_blocks:
data_block["prim_path"] = str(prim_path)
###########################################################
# Create a text object to display the stage's custom data.
###########################################################
stage = import_context.get_stage()
if stage is None:
return False
# Get the custom data.
rootLayer = stage.GetRootLayer()
customData = rootLayer.customLayerData
# Create a text object to display the stage path
# and custom data dictionary entries.
bpy.ops.object.text_add()
ob = bpy.context.view_layer.objects.active
if (ob is None) or (ob.data is None):
return False
ob.name = "layer_data"
ob.data.name = "layer_data"
# The stage root path is the first line.
text = rootLayer.realPath
# Append key/value strings, enforcing text wrapping.
for item in customData.items():
print(item)
text += '\n'
line = str(item[0]) + ': ' + str(item[1])
text += textwrap.fill(line, width=80)
ob.data.body = text
return True
@staticmethod
def material_import_poll(import_context, usd_material):
"""
Return True if the given USD material can be converted.
Return False otherwise.
"""
# We can convert MaterialX.
surf_output = usd_material.GetSurfaceOutput("mtlx")
return bool(surf_output)
@staticmethod
def on_material_import(import_context, bl_material, usd_material):
"""
Import a simple mtlx material. Just handle the base_color input
of a ND_standard_surface_surfaceshader.
"""
# We must confirm that we can handle this material.
surf_output = usd_material.GetSurfaceOutput("mtlx")
if not surf_output:
return False
if not surf_output.HasConnectedSource():
return False
# Get the connected surface output source.
source = surf_output.GetConnectedSource()
# Get the shader prim from the source
shader = UsdShade.Shader(source[0])
shader_id = shader.GetShaderId()
if shader_id != "ND_standard_surface_surfaceshader":
return False
color_attr = shader.GetInput("base_color")
if color_attr is None:
return False
# Create the node tree
nodes = bl_material.node_tree.nodes
output = nodes.new(type="ShaderNodeOutputMaterial")
bsdf = nodes.new(type="ShaderNodeBsdfPrincipled")
bsdf.location[0] -= 1.5 * bsdf.width
bl_material.node_tree.links.new(output.inputs["Surface"], bsdf.outputs["BSDF"])
bsdf_base_color_input = bsdf.inputs['Base Color']
# Try to set the default color value.
# Get the authored default value
color = color_attr.Get()
if color is None:
return False
bsdf_base_color_input.default_value = (color[0], color[1], color[2], 1)
return True
def register():
bpy.utils.register_class(USDHookExample)
def unregister():
bpy.utils.unregister_class(USDHookExample)
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,39 @@
"""
**Save 3D Viewport to a PNG**
Capture the 3D viewport's main region from the current window
and write it to a PNG file using :mod:`imbuf`.
"""
import bpy
import imbuf
window = bpy.context.window
# Locate the 3D viewport (if any).
region = None
for area in window.screen.areas:
if area.type == 'VIEW_3D':
for region_iter in area.regions:
if region_iter.type == 'WINDOW':
region = region_iter
break
break
if region is not None:
# The end coordinate is not inclusive, like Python slicing.
region_rect = (
(region.x, region.y),
(region.x + region.width, region.y + region.height),
)
pixels = window.screenshot(region=region_rect)
height, width = pixels.shape[0], pixels.shape[1]
ibuf = imbuf.new((width, height))
ibuf.file_type = 'PNG'
with ibuf.with_buffer(write=True) as buf:
# The cast produces a zero-copy 1-D view of the same bytes.
# Currently only 1-D copies are supported by Python.
buf.cast('B')[:] = pixels.cast('B')
imbuf.write(ibuf, filepath="/tmp/viewport.png")

View File

@@ -0,0 +1,45 @@
"""
This method is used from the operators ``invoke`` callback
which must then return ``{'RUNNING_MODAL'}``.
Accepting the file selector will run the operators ``execute`` callback.
The following properties are supported:
``filepath``: ``bpy.props.StringProperty(subtype='FILE_PATH')``
Represents the absolute path to the file.
``dirpath``: ``bpy.props.StringProperty(subtype='DIR_PATH')``
Represents the absolute path to the directory.
``filename``: ``bpy.props.StringProperty(subtype='FILE_NAME')``
Represents the filename without the leading directory.
``files``: ``bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement)``
When present in the operator this collection includes all selected files.
``filter_glob``: ``bpy.props.StringProperty(default="*.ext")``
When present in the operator and it's not empty,
it will be used as a file filter (example value: ``*.zip;*.py;*.exe``).
``check_existing``: ``bpy.props.BoolProperty()``
If this property is present and set to ``True``,
the operator will warn if the provided file-path already exists
by highlighting the filename input field in red.
.. warning::
After opening the file-browser the user may continue to use Blender,
this means it is possible for the user to change the context in ways
that would cause the operators ``poll`` function to fail.
Unless the operator reads all necessary data from the context before the file-selector is opened,
it is recommended for operators to check the ``poll`` function from ``execute``
to ensure the context is still valid.
Example from the body of an operators ``execute`` function:
.. code-block:: python
if self.options.is_invoke:
# The context may have changed since invoking the file selector.
if not self.poll(context):
self.report({'ERROR'}, "Invalid context")
return {'CANCELLED'}
"""

View File

@@ -0,0 +1,14 @@
"""
Popup menus can be useful for creating menus without having to register menu classes.
Note that they will not block the scripts execution, so the caller can't wait for user input.
"""
import bpy
def draw(self, context):
self.layout.label(text="Hello World")
bpy.context.window_manager.popup_menu(draw, title="Greeting", icon='INFO')

View File

@@ -0,0 +1,18 @@
"""
Only works for 'basic type' properties (bool, int and float)!
Multi-dimensional arrays (like array of vectors) will be flattened into seq.
"""
import bpy
mesh = bpy.context.object.data
collection = mesh.vertices
# Allocate a flat list for the `co` property (X, Y, Z per vertex).
coords = [0.0] * len(collection) * 3
# Fast access.
collection.foreach_get("co", coords)
# Python equivalent (per-element iteration is much slower).
for i, vert in enumerate(collection):
coords[i * 3], coords[i * 3 + 1], coords[i * 3 + 2] = vert.co

View File

@@ -0,0 +1,21 @@
"""
Only works for 'basic type' properties (bool, int and float)!
seq must be uni-dimensional, multi-dimensional arrays (like array of vectors) will be re-created from it.
"""
import bpy
mesh = bpy.context.object.data
collection = mesh.vertices
# Flatten all Z coordinates to zero (X, Y, Z per vertex).
coords = [0.0] * len(collection) * 3
collection.foreach_get("co", coords)
for i in range(2, len(coords), 3):
coords[i] = 0.0
# Fast assignment.
collection.foreach_set("co", coords)
# Python equivalent (per-element iteration is much slower).
for i, vert in enumerate(collection):
vert.co = (coords[i * 3], coords[i * 3 + 1], coords[i * 3 + 2])

View File

@@ -0,0 +1,30 @@
"""
.. note::
Properties defined at run-time store the values of the properties as custom-properties.
This method checks if the underlying data exists, causing the property to be considered *set*.
A common pattern for operators is to calculate a value for the properties
that have not had their values explicitly set by the caller
(where the caller could be a key-binding, menu-items or Python script for example).
In the case of executing operators multiple times, values are re-used from the previous execution.
For example: subdividing a mesh with a smooth value of 1.0 will keep using
that value on subsequent calls to subdivision, unless the operator is called with
that property set to a different value.
This behavior can be disabled using the ``SKIP_SAVE`` option when the property is declared (see: :mod:`bpy.props`).
The ``ghost`` argument allows detecting how a value from a previous execution is handled.
- When true: The property is considered unset even if the value from a previous call is used.
- When false: The existence of any values causes ``is_property_set`` to return true.
While this argument should typically be omitted, there are times when
it's important to know if a value is anything besides the default.
For example, the previous value may have been scaled by the scene's unit scale.
In this case scaling the value multiple times would cause problems, so the ``ghost`` argument should be false.
"""

View File

@@ -0,0 +1,11 @@
"""
This is the most simple example of inserting a keyframe from Python.
"""
import bpy
obj = bpy.context.object
# Set the keyframe at frame 1.
obj.location = (3.0, 4.0, 10.0)
obj.keyframe_insert(data_path="location", frame=1)

View File

@@ -0,0 +1,36 @@
"""
Note that when keying data paths which contain nested properties this must be
done from the :class:`ID` subclass, in this case the :class:`Armature` rather
than the bone.
"""
import bpy
from bpy.props import (
FloatProperty,
PointerProperty,
)
# Define a nested property.
class MyPropGroup(bpy.types.PropertyGroup):
nested: FloatProperty(name="Nested", default=0.0)
# Register it so its available for all bones.
bpy.utils.register_class(MyPropGroup)
bpy.types.Bone.my_prop = PointerProperty(
type=MyPropGroup,
name="MyProp",
)
# Get a bone.
obj = bpy.data.objects["Armature"]
arm = obj.data
# Set the keyframe at frame 1.
arm.bones["Bone"].my_prop.nested = 10
arm.keyframe_insert(
data_path='bones["Bone"].my_prop.nested',
frame=1,
group="Nested Group",
)

View File

@@ -0,0 +1,91 @@
"""
**Custom Commands**
Registering commands makes it possible to conveniently expose command line
functionality via commands passed to (``-c`` / ``--command``).
"""
import os
import bpy
def sysinfo_print():
"""
Report basic system information.
"""
import pprint
import platform
import textwrap
width = 80
indent = 2
print("Blender {:s}".format(bpy.app.version_string))
print("Running on: {:s}-{:s}".format(platform.platform(), platform.machine()))
print("Processors: {!r}".format(os.cpu_count()))
print()
# Dump `bpy.app`.
for attr in dir(bpy.app):
if attr.startswith("_"):
continue
# Overly verbose.
if attr in {"handlers", "build_cflags", "build_cxxflags"}:
continue
value = getattr(bpy.app, attr)
if attr.startswith("build_"):
pass
elif isinstance(value, tuple):
pass
else:
# Otherwise ignore.
continue
if isinstance(value, bytes):
value = value.decode("utf-8", errors="ignore")
if isinstance(value, str):
pass
elif isinstance(value, tuple) and hasattr(value, "__dir__"):
value = {
attr_sub: value_sub
for attr_sub in dir(value)
# Exclude built-ins.
if not attr_sub.startswith(("_", "n_"))
# Exclude methods.
if not callable(value_sub := getattr(value, attr_sub))
}
value = pprint.pformat(value, indent=0, width=width)
else:
value = pprint.pformat(value, indent=0, width=width)
print("{:s}:\n{:s}\n".format(attr, textwrap.indent(value, " " * indent)))
def sysinfo_command(argv):
if argv and argv[0] == "--help":
print("Print system information & exit!")
return 0
sysinfo_print()
return 0
cli_commands = []
def register():
cli_commands.append(bpy.utils.register_cli_command("sysinfo", sysinfo_command))
def unregister():
for cmd in cli_commands:
bpy.utils.unregister_cli_command(cmd)
cli_commands.clear()
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,73 @@
"""
**Using Python Argument Parsing**
This example shows how the Python ``argparse`` module can be used with a custom command.
Using ``argparse`` is generally recommended as it has many useful utilities and
generates a ``--help`` message for your command.
"""
import os
import sys
import bpy
def argparse_create():
import argparse
parser = argparse.ArgumentParser(
prog=os.path.basename(sys.argv[0]) + " --command keyconfig_export",
description="Write key-configuration to a file.",
)
parser.add_argument(
"-o", "--output",
dest="output",
metavar='OUTPUT',
type=str,
help="The path to write the keymap to.",
required=True,
)
parser.add_argument(
"-a", "--all",
dest="all",
action="store_true",
help="Write all key-maps (not only customized key-maps).",
required=False,
)
return parser
def keyconfig_export(argv):
parser = argparse_create()
args = parser.parse_args(argv)
# Ensure the key configuration is loaded in background mode.
bpy.utils.keyconfig_init()
bpy.ops.preferences.keyconfig_export(
filepath=args.output,
all=args.all,
)
return 0
cli_commands = []
def register():
cli_commands.append(bpy.utils.register_cli_command("keyconfig_export", keyconfig_export))
def unregister():
for cmd in cli_commands:
bpy.utils.unregister_cli_command(cmd)
cli_commands.clear()
if __name__ == "__main__":
register()

View File

@@ -0,0 +1,187 @@
"""
Geometry Batches
++++++++++++++++
Geometry is drawn in batches.
A batch contains the necessary data to perform the drawing.
That includes an obligatory *Vertex Buffer* and an optional *Index Buffer*,
each of which is described in more detail in the following sections.
A batch also defines a draw type.
Typical draw types are ``POINTS``, ``LINES`` and ``TRIS``.
The draw type determines how the data will be interpreted and drawn.
Vertex Buffers
++++++++++++++
A *Vertex Buffer Object* (VBO) (:class:`gpu.types.GPUVertBuf`)
is an array that contains the vertex attributes needed for drawing using a specific shader.
Typical vertex attributes are *location*, *normal*, *color*, and *uv*.
Every vertex buffer has a *Vertex Format* (:class:`gpu.types.GPUVertFormat`)
and a length corresponding to the number of vertices in the buffer.
A vertex format describes the attributes stored per vertex and their types.
The following code demonstrates the creation of a vertex buffer that contains 6 vertices.
For each vertex 2 attributes will be stored: The position and the normal.
.. code-block:: python
import gpu
vertex_positions = [(0, 0, 0), ...]
vertex_normals = [(0, 0, 1), ...]
fmt = gpu.types.GPUVertFormat()
fmt.attr_add(id="pos", comp_type='F32', len=3, fetch_mode='FLOAT')
fmt.attr_add(id="normal", comp_type='F32', len=3, fetch_mode='FLOAT')
vbo = gpu.types.GPUVertBuf(len=6, format=fmt)
vbo.attr_fill(id="pos", data=vertex_positions)
vbo.attr_fill(id="normal", data=vertex_normals)
This vertex buffer could be used to draw 6 points, 3 separate lines, 5 consecutive lines, 2 separate triangles, ...
E.g. in the case of lines, each two consecutive vertices define a line.
The type that will actually be drawn is determined when the batch is created later.
Index Buffers
+++++++++++++
Often triangles and lines share one or more vertices.
With only a vertex buffer one would have to store all attributes for the these vertices multiple times.
This is very inefficient because in a connected triangle mesh every vertex is used 6 times on average.
A more efficient approach would be to use an *Index Buffer* (IBO) (:class:`gpu.types.GPUIndexBuf`),
sometimes referred to as *Element Buffer*.
An *Index Buffer* is an array that references vertices based on their index in the vertex buffer.
For instance, to draw a rectangle composed of two triangles, one could use an index buffer.
.. code-block:: python
positions = (
(-1, 1), (1, 1),
(-1, -1), (1, -1))
indices = ((0, 1, 2), (2, 1, 3))
ibo = gpu.types.GPUIndexBuf(type='TRIS', seq=indices)
Here the first tuple in ``indices`` describes which vertices should be used for the first triangle
(same for the second tuple).
Note how the diagonal vertices 1 and 2 are shared between both triangles.
Shaders
+++++++
A shader is a program that runs on the GPU (written in GLSL in our case).
There are multiple types of shaders.
The most important ones are *Vertex Shaders* and *Fragment Shaders*.
Typically multiple shaders are linked together into a *Program*.
However, in the Blender Python API the term *Shader* refers to an OpenGL Program.
Every :class:`gpu.types.GPUShader` consists of a vertex shader, a fragment shader and an optional geometry shader.
For common drawing tasks there are some built-in shaders accessible from :class:`gpu.shader.from_builtin`
with an identifier such as ``UNIFORM_COLOR`` or ``FLAT_COLOR``. There are specific builtin shaders for
drawing triangles, lines and points.
Every shader defines a set of attributes and uniforms that have to be set in order to use the shader.
Attributes are properties that are set using a vertex buffer and can be different for individual vertices.
Uniforms are properties that are constant per draw call.
They can be set using the ``shader.uniform_*`` functions after the shader has been bound.
.. note::
It is important to note that GLSL sources are reinterpreted to MSL (Metal Shading Language)
on Apple operating systems.
This uses a small compatibility layer that does not cover the whole GLSL language specification.
Here is a list of differences to keep in mind when targeting compatibility with Apple platforms:
- The only matrix constructors available are:
- diagonal scalar (example: ``mat2(1)``)
- all scalars (example: ``mat2(1, 0, 0, 1)``)
- column vector (example: ``mat2(vec2(1,0), vec2(0,1))``)
- reshape constructors work only for square matrices (example: ``mat3(mat4(1))``)
- ``vertex``, ``fragment`` and ``kernel`` are reserved keywords.
- all types and keywords defined by the
`MSL specification <https://developer.apple.com/metal/Metal-Shading-Language-Specification.pdf>`__
are reserved keywords and should not be used.
Batch Creation
++++++++++++++
Batches can be created by first manually creating VBOs and IBOs.
However, it is recommended to use the :class:`gpu_extras.batch.batch_for_shader` function.
It makes sure that all the vertex attributes necessary for a specific shader are provided.
Consequently, the shader has to be passed to the function as well.
When using this function one rarely has to care about the vertex format, VBOs and IBOs created in the background.
This is still something one should know when drawing stuff though.
Since batches can be drawn multiple times, they should be cached and reused whenever possible.
Offscreen Rendering
+++++++++++++++++++
What one can see on the screen after rendering is called the *Front Buffer*.
When draw calls are issued, batches are drawn on a *Back Buffer* that will only be displayed
when all drawing is done and the current back buffer will become the new front buffer.
Sometimes, one might want to draw the batches into a distinct buffer that could be used as
texture to display on another object or to be saved as image on disk.
This is called Offscreen Rendering.
In Blender Offscreen Rendering is done using the :class:`gpu.types.GPUOffScreen` type.
.. warning::
:class:`gpu.types.GPUOffScreen` objects are bound to the OpenGL context they have been created in.
This means that once Blender discards this context (i.e. the window is closed),
the offscreen instance will be freed.
Examples
++++++++
To try these examples, just copy them into Blender's text editor and execute them.
To keep the examples relatively small, they just register a draw function that can't easily be removed anymore.
Blender has to be restarted in order to delete the draw handlers.
3D Points with Single Color
"""
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
coords = [(1, 1, 1), (-2, 0, 0), (-2, -1, 3), (0, 1, 1)]
shader = gpu.shader.from_builtin('POINT_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'POINTS', {"pos": coords})
def draw():
shader.uniform_float("color", (1, 1, 0, 1))
gpu.state.point_size_set(4.5)
batch.draw(shader)
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')
"""
3D Lines with Single Color
--------------------------
"""
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
coords = [(1, 1, 1), (-2, 0, 0), (-2, -1, 3), (0, 1, 1)]
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'LINES', {"pos": coords})
def draw():
shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
shader.uniform_float("lineWidth", 4.5)
shader.uniform_float("color", (1, 1, 0, 1))
batch.draw(shader)
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')

View File

@@ -0,0 +1,67 @@
"""
Custom Shader for dotted 3D Line
--------------------------------
In this example the arc length (distance to the first point on the line) is calculated in every vertex.
Between the vertex and fragment shader that value is automatically interpolated
for all points that will be visible on the screen.
In the fragment shader the ``sin`` of the arc length is calculated.
Based on the result a decision is made on whether the fragment should be drawn or not.
"""
import bpy
import gpu
from random import random
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth('FLOAT', "v_ArcLength")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant('MAT4', "u_ViewProjectionMatrix")
shader_info.push_constant('FLOAT', "u_Scale")
shader_info.vertex_in(0, 'VEC3', "position")
shader_info.vertex_in(1, 'FLOAT', "arcLength")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, 'VEC4', "FragColor")
shader_info.vertex_source(
"void main()"
"{"
" v_ArcLength = arcLength;"
" gl_Position = u_ViewProjectionMatrix * vec4(position, 1.0f);"
"}"
)
shader_info.fragment_source(
"void main()"
"{"
" if (step(sin(v_ArcLength * u_Scale), 0.5) == 1) discard;"
" FragColor = vec4(1.0);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
coords = [Vector((random(), random(), random())) * 5 for _ in range(5)]
arc_lengths = [0.0]
for a, b in zip(coords[:-1], coords[1:]):
arc_lengths.append(arc_lengths[-1] + (a - b).length)
batch = batch_for_shader(
shader, 'LINE_STRIP',
{"position": coords, "arcLength": arc_lengths},
)
def draw():
matrix = bpy.context.region_data.perspective_matrix
shader.uniform_float("u_ViewProjectionMatrix", matrix)
shader.uniform_float("u_Scale", 10)
batch.draw(shader)
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')

View File

@@ -0,0 +1,96 @@
"""
Custom compute shader (using image store) and vertex/fragment shader
--------------------------------------------------------------------
This is an example of how to use a custom compute shader
to write to a texture and then use that texture in a vertex/fragment shader.
The expected result is a 2x2 plane (size of the default cube),
which changes color from a green-black gradient to a green-red gradient,
based on current time.
"""
import bpy
import gpu
from mathutils import Matrix
from gpu_extras.batch import batch_for_shader
import time
start_time = time.time()
size = 128
texture = gpu.types.GPUTexture((size, size), format='RGBA32F')
# Create the compute shader to write to the texture.
compute_shader_info = gpu.types.GPUShaderCreateInfo()
compute_shader_info.image(0, 'RGBA32F', "FLOAT_2D", "img_output", qualifiers={"WRITE"})
compute_shader_info.compute_source('''
void main()
{
vec4 pixel = vec4(
sin(time / 1.0),
gl_GlobalInvocationID.y/128.0,
0.0,
1.0
);
imageStore(img_output, ivec2(gl_GlobalInvocationID.xy), pixel);
}''')
compute_shader_info.push_constant('FLOAT', "time")
compute_shader_info.local_group_size(1, 1)
compute_shader = gpu.shader.create_from_info(compute_shader_info)
# Create the shader to draw the texture.
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth('VEC2', "uvInterp")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant('MAT4', "viewProjectionMatrix")
shader_info.push_constant('MAT4', "modelMatrix")
shader_info.sampler(0, 'FLOAT_2D', "img_input")
shader_info.vertex_in(0, 'VEC2', "position")
shader_info.vertex_in(1, 'VEC2', "uv")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, 'VEC4', "FragColor")
shader_info.vertex_source(
"void main()"
"{"
" uvInterp = uv;"
" gl_Position = viewProjectionMatrix * modelMatrix * vec4(position, 0.0, 1.0);"
"}"
)
shader_info.fragment_source(
"void main()"
"{"
" FragColor = texture(img_input, uvInterp);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
batch = batch_for_shader(
shader, 'TRI_STRIP',
{
"position": ((-1, -1), (1, -1), (-1, 1), (1, 1)),
"uv": ((0, 0), (1, 0), (0, 1), (1, 1)),
},
)
def draw():
shader.uniform_float("modelMatrix", Matrix.Translation((0, 0, 0)) @ Matrix.Scale(1, 4))
shader.uniform_float("viewProjectionMatrix", bpy.context.region_data.perspective_matrix)
shader.uniform_sampler("img_input", texture)
batch.draw(shader)
compute_shader.image('img_output', texture)
compute_shader.uniform_float("time", time.time() - start_time)
gpu.compute.dispatch(compute_shader, 128, 128, 1)
def drawTimer():
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
area.tag_redraw()
return 1.0 / 60.0
bpy.app.timers.register(drawTimer)
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')

View File

@@ -0,0 +1,50 @@
"""
Triangle with Custom Shader
---------------------------
"""
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
vert_out = gpu.types.GPUStageInterfaceInfo("my_interface")
vert_out.smooth('VEC3', "pos")
shader_info = gpu.types.GPUShaderCreateInfo()
shader_info.push_constant('MAT4', "viewProjectionMatrix")
shader_info.push_constant('FLOAT', "brightness")
shader_info.vertex_in(0, 'VEC3', "position")
shader_info.vertex_out(vert_out)
shader_info.fragment_out(0, 'VEC4', "FragColor")
shader_info.vertex_source(
"void main()"
"{"
" pos = position;"
" gl_Position = viewProjectionMatrix * vec4(position, 1.0f);"
"}"
)
shader_info.fragment_source(
"void main()"
"{"
" FragColor = vec4(pos * brightness, 1.0);"
"}"
)
shader = gpu.shader.create_from_info(shader_info)
del vert_out
del shader_info
coords = [(1, 1, 1), (2, 0, 0), (-2, -1, 3)]
batch = batch_for_shader(shader, 'TRIS', {"position": coords})
def draw():
matrix = bpy.context.region_data.perspective_matrix
shader.uniform_float("viewProjectionMatrix", matrix)
shader.uniform_float("brightness", 0.5)
batch.draw(shader)
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')

View File

@@ -0,0 +1,31 @@
"""
Wireframe Cube using Index Buffer
---------------------------------
"""
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
coords = (
(-1, -1, -1), (+1, -1, -1),
(-1, +1, -1), (+1, +1, -1),
(-1, -1, +1), (+1, -1, +1),
(-1, +1, +1), (+1, +1, +1))
indices = (
(0, 1), (0, 2), (1, 3), (2, 3),
(4, 5), (4, 6), (5, 7), (6, 7),
(0, 4), (1, 5), (2, 6), (3, 7))
shader = gpu.shader.from_builtin('POLYLINE_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'LINES', {"pos": coords}, indices=indices)
def draw():
shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
shader.uniform_float("lineWidth", 4.5)
shader.uniform_float("color", (1, 0, 0, 1))
batch.draw(shader)
bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_VIEW')

Some files were not shown because too many files have changed in this diff Show More