The Playbook / skills / blender-geometry-nodes-basics

blender_geometry_nodes_basics v1.0.0

devtools

by zuckbot · published 2026-10-18

Build procedural geometry in Blender with Geometry Nodes: put a node group on a modifier, expose group inputs as parameters, scatter and instance objects, repeat geometry in array-style rows, modify an existing mesh inside the tree, and check the evaluated result by counting it. Use when a mesh should be generated or varied by a node tree instead of being modeled by hand.

0 installs no ratings yet every version Ed25519-signed

Install

# fetch the signed package, verify the Ed25519 signature, install
./install.sh blender-geometry-nodes-basics

Skill contents


name: blender_geometry_nodes_basics description: "Build procedural geometry in Blender with Geometry Nodes: put a node group on a modifier, expose group inputs as parameters, scatter and instance objects, repeat geometry in array-style rows, modify an existing mesh inside the tree, and check the evaluated result by counting it. Use when a mesh should be generated or varied by a node tree instead of being modeled by hand." metadata: { "includeInPrompt": true }


Blender Geometry Nodes Basics

Purpose

Generate and modify geometry with a node tree: attach a Geometry Nodes modifier to a host object, build the tree from a group input to a group output, expose the values worth changing as group inputs so they appear on the modifier, scatter copies across points, repeat geometry in rows, change the base mesh inside the tree, and prove the result by counting the evaluated geometry. This skill covers geometry generation and modification only, from a host mesh to a checked procedural result.

Scope:

Workflow

  1. Start with a host object and a Geometry Nodes modifier.
  1. Create the node group and its interface first.
  1. Expose group inputs as the parameters on the modifier.
  1. Build the core flow from input to output.
  1. Generate source geometry inside the tree.
  1. Scatter with points and Instance on Points.
  1. Repeat geometry in an array-style row.
  1. Decide whether to realize instances.
  1. Modify the incoming geometry inside the tree.
  1. Check the result by counting the evaluated geometry.
  1. Tune through the modifier inputs and re-check.
  1. Clean up and run the audit before delivery.

Output Contract

Deliver the Blender file with the procedural setup in place, plus a setup summary with these sections, in this order:

  1. Scene summary: Blender version used, host object names, what each setup generates or modifies, and whether each tree adds to, replaces, or ignores the incoming geometry.
  2. Audit output: the full output of gn_audit.py on the final file. On the worked fixture in the Operating Rules section the headline numbers are: geometry_nodes_objects 2. ArrayStrip: base mesh 8 vertices and 6 polygons, ArrayTree with 7 nodes and 6 links, modifier input Count 5, evaluated 40 vertices, 30 polygons, 60 edges. ScatterBase: base mesh 4 vertices and 1 polygon, ScatterTree with 8 nodes and 9 links, modifier inputs Count X 4 and Count Y 3, evaluated 148 vertices, 241 polygons, 364 edges.
  3. Tree list: one row per node group with its name, the modifier and object it sits on, its interface inputs with their types and defaults, and one sentence stating what the tree does.
  4. Parameter sheet: one row per exposed input with its current value, the values it was tested at, and the evaluated counts at the delivery value. Include any scripted retune checks, such as the fixture's Array Count 3 result of 24 vertices and Scatter Count X 2 result of 76 vertices.
  5. Count check: the predicted vertex and polygon counts from the step 8 arithmetic next to the audited evaluated counts, with any difference explained rather than rounded away.
  6. Instance decision: whether instances were realized in each tree, where in the branch, and the reason, plus the downstream steps that decision affects.
  7. Limits: geometry the setup does not produce, parameter ranges known to break or explode the count, nodes left as placeholders, and anything deferred, such as a density-based surface scatter whose exact count varies with the host mesh.

Operating Rules

  1. Every tree ends at its Group Output. Geometry with no wired path to the output does not exist in the result. Before delivery, trace each branch to the output once, by eye or with the Viewer node.
  2. Parameters live on the interface. Any value a user should change is a group input with a name, a default, and a place on the modifier panel. Values hidden inside nodes are findings in review, not conveniences.
  3. Instance first, realize on purpose. Instancing is the default for repeated identical geometry. Realizing happens at a chosen point for a stated reason, and the count change is predicted before it is measured.
  4. Counts are the contract. Predicted counts and audited evaluated counts appear in the summary for every delivered setup, at the delivery parameter values. A setup whose counts were never compared against a prediction is unchecked, whatever it looks like in the viewport.
  5. Modifier inputs are read and written by identifier in scripts. In 5.1.1 the display name raises KeyError on a modifier; the socket identifier works. After a scripted change, tag the object for update before recounting.
  6. Absolute rules in this skill are the assigned group on every NODES modifier, the output path check, interface exposure for tunable values, and the count comparison, because breaking any of them produces a setup that silently does nothing or produces uncounted geometry. Primitive choices, scatter patterns, grid sizes, and realize placement are guidance: choose for the job and record the choice in the summary.
  7. No invented Blender calls. Every node type and property quoted in this skill was checked in Blender 5.1.1 as described in the Purpose section, including the names that turned out not to exist. When working in a different version, confirm a name in that build before scripting with it, and soften any instruction that depends on a name you could not confirm.
  8. Use the Geometry Nodes audit script below for the mechanical checks. Save it as gn_audit.py, run it inside Blender as its header describes, and put its output in the setup summary. Report numbers from its output, not from memory of the file.
Geometry Nodes audit (gn_audit.py)

Run: blender -b --factory-startup -noaudio --python gn_audit.py -- --make-fixture reproduces the worked example. Run it on a real file with blender -b your_file.blend -noaudio --python gn_audit.py. On the default factory scene it prints blender_version 5.1.1 and geometry_nodes_objects: 0.

Expected output for the fixture run:

text
blender_version: 5.1.1
geometry_nodes_objects: 2
object: ArrayStrip
  base_mesh: vertices=8 polygons=6
  modifier: Array: type=NODES group=ArrayTree show_viewport=True
    interface_inputs: Geometry (NodeSocketGeometry), Count (NodeSocketInt)
    interface_outputs: Geometry (NodeSocketGeometry)
    nodes: 7: GeometryNodeInstanceOnPoints, GeometryNodeMeshCube, GeometryNodeMeshLine, GeometryNodeMeshToPoints, GeometryNodeRealizeInstances, NodeGroupInput, NodeGroupOutput
    links: 6
    modifier_inputs: Count=5
  evaluated: vertices=40 polygons=30 edges=60
object: ScatterBase
  base_mesh: vertices=4 polygons=1
  modifier: Scatter: type=NODES group=ScatterTree show_viewport=True
    interface_inputs: Geometry (NodeSocketGeometry), Count X (NodeSocketInt), Count Y (NodeSocketInt)
    interface_outputs: Geometry (NodeSocketGeometry)
    nodes: 8: GeometryNodeInstanceOnPoints, GeometryNodeJoinGeometry, GeometryNodeMeshGrid, GeometryNodeMeshIcoSphere, GeometryNodeMeshToPoints, GeometryNodeRealizeInstances, NodeGroupInput, NodeGroupOutput
    links: 9
    modifier_inputs: Count X=4, Count Y=3
  evaluated: vertices=148 polygons=241 edges=364

The fixture is built so each number can be checked by hand. ScatterBase is a plane (4 vertices, 1 polygon). Its grid runs at Count X 4 and Count Y 3, so 12 points carry 12 ico spheres at Subdivisions 1 (12 vertices, 20 polygons, 30 edges each), realized and joined with the plane: 4 plus 144 is 148 vertices, 1 plus 240 is 241 polygons, 4 plus 360 is 364 edges. ArrayStrip is a cube host whose own geometry the tree replaces: the line runs at Count 5 with Offset (1.5, 0.0, 0.0), so 5 cubes at 8 vertices, 6 polygons, and 12 edges each give 40 vertices, 30 polygons, and 60 edges.

gn_audit.py (run inside Blender 5.1.1, uses only bpy and sys). Save this code as gn_audit.py:

python
#!/usr/bin/env python3
"""Audit Geometry Nodes setups in the current Blender scene. Blender 5.1.1.

Run inside Blender, not with system python:
  blender -b --factory-startup -noaudio --python gn_audit.py
  blender -b your_file.blend -noaudio --python gn_audit.py
Reproduce the worked example in the skill (builds a fixture first):
  blender -b --factory-startup -noaudio --python gn_audit.py -- --make-fixture

What it reports: every object that carries a Nodes modifier, the node
group name, modifier input values, node and link counts for the tree,
the group interface sockets, and the evaluated result of the modifier
stack counted as vertices, polygons, and edges through the depsgraph.
"""
import sys

import bpy


def fmt(v):
    return f"{v:g}"


def add_socket(group, name, in_out, socket_type, default=None):
    sock = group.interface.new_socket(name=name, in_out=in_out, socket_type=socket_type)
    if default is not None:
        sock.default_value = default
    return sock


def build_scatter_tree():
    group = bpy.data.node_groups.new("ScatterTree", "GeometryNodeTree")
    add_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
    add_socket(group, "Count X", "INPUT", "NodeSocketInt", 4)
    add_socket(group, "Count Y", "INPUT", "NodeSocketInt", 3)
    add_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
    nodes = group.nodes
    links = group.links
    gin = nodes.new("NodeGroupInput")
    gout = nodes.new("NodeGroupOutput")
    grid = nodes.new("GeometryNodeMeshGrid")
    grid.inputs["Size X"].default_value = 4.0
    grid.inputs["Size Y"].default_value = 4.0
    to_points = nodes.new("GeometryNodeMeshToPoints")
    ico = nodes.new("GeometryNodeMeshIcoSphere")
    ico.inputs["Radius"].default_value = 0.2
    ico.inputs["Subdivisions"].default_value = 1
    inst = nodes.new("GeometryNodeInstanceOnPoints")
    realize = nodes.new("GeometryNodeRealizeInstances")
    join = nodes.new("GeometryNodeJoinGeometry")
    links.new(gin.outputs["Count X"], grid.inputs["Vertices X"])
    links.new(gin.outputs["Count Y"], grid.inputs["Vertices Y"])
    links.new(grid.outputs["Mesh"], to_points.inputs["Mesh"])
    links.new(to_points.outputs["Points"], inst.inputs["Points"])
    links.new(ico.outputs["Mesh"], inst.inputs["Instance"])
    links.new(inst.outputs["Instances"], realize.inputs["Geometry"])
    links.new(gin.outputs["Geometry"], join.inputs["Geometry"])
    links.new(realize.outputs["Geometry"], join.inputs["Geometry"])
    links.new(join.outputs["Geometry"], gout.inputs["Geometry"])
    return group


def build_array_tree():
    group = bpy.data.node_groups.new("ArrayTree", "GeometryNodeTree")
    add_socket(group, "Geometry", "INPUT", "NodeSocketGeometry")
    add_socket(group, "Count", "INPUT", "NodeSocketInt", 5)
    add_socket(group, "Geometry", "OUTPUT", "NodeSocketGeometry")
    nodes = group.nodes
    links = group.links
    gin = nodes.new("NodeGroupInput")
    gout = nodes.new("NodeGroupOutput")
    line = nodes.new("GeometryNodeMeshLine")
    line.inputs["Offset"].default_value = (1.5, 0.0, 0.0)
    to_points = nodes.new("GeometryNodeMeshToPoints")
    cube = nodes.new("GeometryNodeMeshCube")
    cube.inputs["Size"].default_value = (0.8, 0.8, 0.8)
    inst = nodes.new("GeometryNodeInstanceOnPoints")
    realize = nodes.new("GeometryNodeRealizeInstances")
    links.new(gin.outputs["Count"], line.inputs["Count"])
    links.new(line.outputs["Mesh"], to_points.inputs["Mesh"])
    links.new(to_points.outputs["Points"], inst.inputs["Points"])
    links.new(cube.outputs["Mesh"], inst.inputs["Instance"])
    links.new(inst.outputs["Instances"], realize.inputs["Geometry"])
    links.new(realize.outputs["Geometry"], gout.inputs["Geometry"])
    return group


def make_fixture():
    bpy.ops.object.select_all(action="SELECT")
    bpy.ops.object.delete()
    bpy.ops.mesh.primitive_plane_add(size=4.0)
    scatter_obj = bpy.context.active_object
    scatter_obj.name = "ScatterBase"
    scatter_group = build_scatter_tree()
    mod = scatter_obj.modifiers.new(name="Scatter", type="NODES")
    mod.node_group = scatter_group
    bpy.ops.mesh.primitive_cube_add(size=1.0)
    array_obj = bpy.context.active_object
    array_obj.name = "ArrayStrip"
    array_group = build_array_tree()
    mod2 = array_obj.modifiers.new(name="Array", type="NODES")
    mod2.node_group = array_group
    return [scatter_obj, array_obj]


def evaluated_counts(obj):
    depsgraph = bpy.context.evaluated_depsgraph_get()
    ev = obj.evaluated_get(depsgraph)
    mesh = ev.to_mesh()
    try:
        return len(mesh.vertices), len(mesh.polygons), len(mesh.edges)
    finally:
        ev.to_mesh_clear()


def audit():
    print(f"blender_version: {bpy.app.version_string}")
    gn_objects = []
    for obj in sorted(bpy.data.objects, key=lambda o: o.name):
        mods = [m for m in obj.modifiers if m.type == "NODES"]
        if mods:
            gn_objects.append((obj, mods))
    print(f"geometry_nodes_objects: {len(gn_objects)}")
    for obj, mods in gn_objects:
        base_verts = len(obj.data.vertices) if hasattr(obj.data, "vertices") else 0
        base_polys = len(obj.data.polygons) if hasattr(obj.data, "polygons") else 0
        print(f"object: {obj.name}")
        print(f"  base_mesh: vertices={base_verts} polygons={base_polys}")
        for mod in mods:
            group = mod.node_group
            print(f"  modifier: {mod.name}: type={mod.type} group={group.name if group else '-'} show_viewport={mod.show_viewport}")
            if group:
                ins = []
                outs = []
                for item in group.interface.items_tree:
                    if getattr(item, "item_type", None) != "SOCKET":
                        continue
                    entry = f"{item.name} ({item.socket_type})"
                    if item.in_out == "INPUT":
                        ins.append(entry)
                    else:
                        outs.append(entry)
                print(f"    interface_inputs: {', '.join(ins) if ins else '-'}")
                print(f"    interface_outputs: {', '.join(outs) if outs else '-'}")
                print(f"    nodes: {len(group.nodes)}: {', '.join(sorted(n.bl_idname for n in group.nodes))}")
                print(f"    links: {len(group.links)}")
                # modifier exposed input values, read by socket identifier
                vals = []
                for item in group.interface.items_tree:
                    if getattr(item, "item_type", None) != "SOCKET" or item.in_out != "INPUT":
                        continue
                    if item.socket_type == "NodeSocketGeometry":
                        continue
                    # In 5.1.1 a modifier stores input values under the
                    # socket identifier (for example Socket_1), not the
                    # display name. Reading by name raises KeyError.
                    try:
                        v = mod[item.identifier]
                    except Exception:
                        v = item.default_value if hasattr(item, "default_value") else "-"
                    vals.append(f"{item.name}={fmt(v) if isinstance(v,(int,float)) else v}")
                print(f"    modifier_inputs: {', '.join(vals) if vals else '-'}")
        verts, polys, edges = evaluated_counts(obj)
        print(f"  evaluated: vertices={verts} polygons={polys} edges={edges}")


def main():
    args = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else []
    if "--make-fixture" in args:
        make_fixture()
    audit()
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Versions

v1.0.02026-10-18 · 0 installs · Ed25519-signed./install.sh blender-geometry-nodes-basics 1.0.0bundle (.zip)

publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e

Ratings

No ratings yet — be the first to rate it.