from nodebpy import NodeGroupBuilder, geometry as g
from nodebpy.types import (
InputFloat,
InputInteger,
InputVector,
InputGeometry,
)Custom Node Groups
Custom node groups let you encapsulate reusable logic into a single node. With NodeGroupBuilder, you define a Python class that:
- Declares inputs and outputs as class-level descriptors.
- Implements the internal graph in a
_build_groupclassmethod. - Works like any other node – full IDE autocomplete, type hints,
>>chaining, and operator support.
The group’s node tree is built once on first use and cached in bpy.data.node_groups, so creating many instances of the same group is cheap.
Anatomy of a Custom Node Group
A custom node group class has four parts:
class MyNode(NodeGroupBuilder):
_name = "My Node" # 1. Name
def __init__(self, value: float = 0.0): # 3. Constructor
super().__init__(value=value)
@classmethod
def _build_group(cls, tree): # 4. Graph logic
return (value + g.Value(1.0)) >> tree.ouptuts.float()1. _name
The display name for the group inside Blender. This is also the key used to cache it in bpy.data.node_groups.
A First Example: Jitter
Let’s start with something visually obvious – a node that randomly displaces each point on a mesh. This is useful any time you want to add organic variation to geometry.
class Jitter(NodeGroupBuilder):
"""Randomly offset each point by a bounded amount."""
_name = "Jitter"
_color_tag = "GEOMETRY"
def __init__(
self,
geometry: InputGeometry = ...,
amount: InputFloat = 0.2,
seed: InputInteger = 0,
):
super().__init__(**{"Geometry": geometry, "Amount": amount, "Seed": seed})
@classmethod
def _build_group(cls, tree):
geometry = tree.inputs.geometry("Geometry")
amount = tree.inputs.float("Amount", 0.2)
seed = tree.inputs.integer("Seed")
offset = g.RandomValue.vector(min=-1, seed=seed) * amount
result = geometry >> g.SetPosition(offset=offset)
_ = result >> tree.outputs.geometry()Let’s see the internal node graph:
with g.tree("JitterInternal") as tree:
_ = Jitter()
treegraph LR
N0("Jitter"):::geometry-node
Now use it like any built-in node – apply it to an ico sphere:
with g.tree("JitterDemo") as tree:
out = tree.outputs.geometry()
_ = g.IcoSphere(subdivisions=4) >> Jitter(amount=0.15) >> out
treegraph LR
N0("Ico Sphere"):::geometry-node
N1("Jitter"):::geometry-node
N2("Group Output"):::default-node
N0 -->|"Mesh->Geometry"| N1
N1 -->|"Geometry->Geometry"| N2
Radial Array
Next, a node that distributes instances in a ring. This is a common pattern for creating wheels, flower petals, gears, and other radially symmetric objects.
from math import tau
class RadialArray(NodeGroupBuilder):
"""Distribute instances evenly around a circle."""
_name = "Radial Array"
_color_tag = "GEOMETRY"
def __init__(
self,
geometry: InputGeometry = ...,
count: InputInteger = 6,
radius: InputFloat = 2.0,
):
super().__init__(geometry=geometry, count=count, radius=radius)
@classmethod
def _build_group(cls, tree):
geometry = tree.inputs.geometry()
count = tree.inputs.integer("Count", 6)
radius = tree.inputs.float("Radius", 2.0)
# Create points arranged in a circle
angle = g.Index() * tau / count
circle_pos = g.CombineXYZ(
x=g.Math.cosine(angle) * radius, y=g.Math.sine(angle) * radius
)
# Place instances at each point, rotated to face outward
rotation = g.CombineXYZ(z=angle)
result = (
g.Points(count)
>> g.SetPosition(position=circle_pos)
>> g.InstanceOnPoints(instance=geometry, rotation=rotation)
)
_ = result >> tree.outputs.geometry()with g.tree("RadialInternal") as tree:
_ = RadialArray()
treegraph LR
N0("Radial Array"):::geometry-node
with g.tree("RadialDemo") as tree:
petal = g.Cone(vertices=4, radius_bottom=0.3, depth=0.8)
_ = (
petal
>> RadialArray(count=8, radius=2.0)
>> g.RealizeInstances()
>> tree.outputs.geometry()
)
treegraph LR
N0("Cone"):::geometry-node
N1("Radial Array"):::geometry-node
N2("Realize Instances"):::geometry-node
N3("Group Output"):::default-node
N0 -->|"Mesh->Geometry"| N1
N1 -->|"Geometry->Geometry"| N2
N2 -->|"Geometry->Geometry"| N3
Composing Groups: Jittered Flower
Custom groups compose naturally – they chain with >>, accept each other’s outputs, and mix with operators just like built-in nodes. Let’s combine Jitter and RadialArray to build a flower-like structure with some organic randomness.
with g.tree("JitteredFlower") as tree:
petals = tree.inputs.integer("Petals", 20, min_value=3, max_value=24)
radius = tree.inputs.float("Radius", 0.5, min_value=0.1)
jitter = tree.inputs.float("Jitter", 0.1)
out = tree.outputs.geometry()
PETAL_LENGTH = 0.8
petal = g.Cube(size=(PETAL_LENGTH, 0.2, 0.1)) >> g.TransformGeometry(
translation=(PETAL_LENGTH / 4, 0, 0)
)
ring = (
petal
>> RadialArray(count=petals, radius=radius)
>> Jitter(amount=jitter)
>> g.RotateInstances(rotation=(0, -tau / 16, 0))
)
center = g.IcoSphere(radius=0.4, subdivisions=3)
_ = g.JoinGeometry(ring, center) >> g.RealizeInstances() >> out
treegraph LR
N0("Group Input"):::default-node
N1("Cube<br/><small>(0.8,0.2,0.1)</small>"):::geometry-node
N2("Transform Geometry<br/><small>(0.2,0,0)</small>"):::geometry-node
N3("Radial Array"):::geometry-node
N4("Jitter"):::geometry-node
N5("Rotate Instances<br/><small>(0,-0.4,0)</small>"):::geometry-node
N6("Ico Sphere"):::geometry-node
N7("Join Geometry"):::geometry-node
N8("Realize Instances"):::geometry-node
N9("Group Output"):::default-node
N1 -->|"Mesh->Geometry"| N2
N0 -->|"Petals->Count"| N3
N0 -->|"Radius->Radius"| N3
N2 -->|"Geometry->Geometry"| N3
N3 -->|"Geometry->Geometry"| N4
N4 -->|"Geometry->Instances"| N5
N6 -->|"Mesh->Geometry"| N7
N5 -->|"Instances->Geometry"| N7
N7 -->|"Geometry->Geometry"| N8
N8 -->|"Geometry->Geometry"| N9
N0 -->|"Jitter->Amount"| N4
Each custom group appears as a single, named node in the tree – keeping the graph readable even as the logic grows.
Class Options
NodeGroupBuilder supports a few class-level options:
| Attribute | Type | Default | Description |
|---|---|---|---|
_name |
str |
(required) | Display name and cache key for the group |
_color_tag |
str |
"NONE" |
Header colour in Blender ("INPUT", "CONVERTER", "GEOMETRY", etc.) |
_warning_propagation |
str |
"ALL" |
How warnings propagate ("ALL", "ERRORS_AND_WARNINGS", "ERRORS", "NONE") |
Summary
| Step | What you write | What it does |
|---|---|---|
Declare i_* |
i_val = InputSpec(partial(s.SocketFloat, "Value")) |
Defines group input socket + node.i_val property |
Declare o_* |
o_result = OutputSpec(partial(s.SocketFloat, "Result")) |
Defines group output socket + node.o_result property |
Write __init__ |
def __init__(self, value=None): super().__init__(value=value) |
Typed constructor for IDE autocomplete |
Write _build_group |
def _build_group(cls, tree, value: s.SocketFloat): ... |
Internal graph logic with typed inputs |