Comparing the API

How does the API compare to other existing solutions?

Several other notable projects have also attempted interfacing with node trees via code. They mostly fit into two categories: either storing & retrieving node trees via code (.json or the bpy API), or authoring node trees with a custom API and syntax. nodebpy mostly fits into the latter category.

Storing node trees as code

Converting node trees to Python API calls or .json gives a robust storage method, but this approach falls down in human authorability / readability. These projects are great for storage but less useful when wanting to write node trees from scratch:

Authoring node trees with code

Two previous projects have made similar approaches to authoring node trees. geometry-script also auto-generated most of its type hinting, code and docs. It uses the approach of method chaining with the . operator, but obfuscates some of the non-linear way of building node trees.

geonodes uses a similar context system for creating and authoring node trees, but doesn’t expose each individual node as its own class the way nodebpy does.

I personally found both of their APIs to not quite fit how I wanted to work, leading to the creation of nodebpy. In comparison, this project is also the only one distributed on PyPI and installable via pip for easier use in other projects.

The sections below reproduce examples from each project’s own documentation side-by-side with the nodebpy equivalent.

geonodes

‘Hello World’

The hello world example from geonodes readme: https://github.com/al1brn/geonodes

from nodebpy import geometry as g

with g.tree("Hello World") as tree:
    height = tree.inputs.float("Height", 3.0)
    omega = tree.inputs.float("Omega", 2.0)

    with g.Frame("Computing the wave"):
        pos = g.Position().o.position
        distance = g.Math.square_root(pos.x**2 + pos.y**2)
        z = height * g.Math.sine(distance * omega) / distance

    with g.Frame("Point offset & smooth"):
        mesh = (
            g.Grid(20, 20, 200, 200)
            >> g.SetPosition(offset=g.CombineXYZ(z=z))
            >> g.SetShadeSmooth.face()
        )

    mesh >> tree.outputs.geometry("Mesh")
from geonodes import *

with GeoNodes("Hello World"):
    height = 3
    omega  = 2

    grid = Mesh.Grid(vertices_x=200, vertices_y=200, size_x=20, size_y=20)
    with Layout("Computing the wave"):
        pos = nd.position
        distance = gnmath.sqrt(pos.x**2 + pos.y**2)
        z = height*gnmath.sin(distance*omega)/distance

    with Layout("Point offset and smoothness"):
        grid.offset = (0, 0, z)
        grid.faces.smooth = True

    grid.out()

geometry-script

README Demo

from nodebpy import geometry as g

with g.tree("Repeat Grid") as tree:
    geometry = tree.inputs.geometry("Geometry")
    width = tree.inputs.integer("Width")
    height = tree.inputs.integer("Height")

    (
        g.Grid(width, height, vertices_x=width, vertices_y=height)
        >> g.MeshToPoints()
        >> g.InstanceOnPoints(instance=geometry)
        >> tree.outputs.geometry("Instances")
    )
from geometry_script import *

@tree("Repeat Grid")
def repeat_grid(geometry: Geometry, width: Int, height: Int):
    g = grid(
        size_x=width, size_y=height,
        vertices_x=width, vertices_y=height
    ).mesh.mesh_to_points()
    return g.instance_on_points(instance=geometry)

Primitive Shapes

from nodebpy import geometry as g

with g.tree("Primitive Shapes") as tree:
    join =  g.JoinGeometry([g.Cube(), g.UVSphere(), g.Cylinder()])
    join >> tree.outputs.geometry("Result")
@tree("Primitive Shapes")
def primitive_shapes():
    yield cube()
    yield uv_sphere()
    yield cylinder().mesh

Voxelise

Example script found here.

from nodebpy import geometry as g

with g.tree("Voxelise") as tree:
    geo = tree.inputs.geometry("Geometry")
    resolution = tree.inputs.float("Resolution", 0.2)

    (
        geo
        >> g.MeshToVolume(interior_band_width=resolution)
        >> g.DistributePointsInVolume(mode="Grid", spacing=resolution)
        >> g.InstanceOnPoints(instance=g.Cube(size=resolution))
        >> tree.outputs.geometry("Result")
    )
from geometry_script import *

@tree("Voxelize")
def voxelize(geometry: Geometry, resolution: Float = 0.2):
    return geometry.mesh_to_volume(
        interior_band_width=resolution,
        fill_volume=False
    ).distribute_points_in_volume(
        mode=DistributePointsInVolume.Mode.DENSITY_GRID,
        spacing=resolution
    ).instance_on_points(
        instance=cube(size=resolution)
    )

City Builder

Example script found here.

from nodebpy import geometry as g

with g.tree("Voxelise") as tree:
    geo = tree.inputs.geometry("Geometry")
    seed = tree.inputs.integer("Seed")
    road_width = tree.inputs.float("Road Width", 0.25)
    size_x = tree.inputs.float("Size X", 5.0)
    size_y = tree.inputs.float("Size Y", 5.0)
    density = tree.inputs.float("Density", 10.0)
    building_size_min = tree.inputs.vector("Building Size Min", (0.1, 0.1, 0.2))
    building_size_max = tree.inputs.vector("Building Size Max", (0.3, 0.3, 1.0))

    curve_mesh = geo >> g.CurveToMesh(
        profile_curve=g.CurveLine(
            start=g.CombineXYZ(x=road_width * -0.5),
            end=g.CombineXYZ(x=road_width * 0.5),
        ),
    )

    building_points = g.Grid(size_x, size_y) >> g.DistributePointsOnFaces(
        density=density, seed=seed
    )

    road_points = geo >> g.CurveToPoints(mode="EVALUATED")
    building_points = g.DeleteGeometry.point(
        building_points,
        selection=g.GeometryProximity(
            target_element="POINTS",
            target=road_points,
            source_position=g.Position().o.position,
        ).o.distance
        < road_width,
    )

    buildings = building_points >> g.InstanceOnPoints(
        instance=g.Cube() >> g.TransformGeometry(translation=(0, 0, 0.5)),
        scale=g.RandomValue.vector(
            min=building_size_min, max=building_size_max, seed=seed
        ),
    )

    g.JoinGeometry((curve_mesh, buildings)) >> tree.outputs.geometry("Result")
from geometry_script import *

@tree("City Builder")
def city_builder(
    geometry: Geometry,
    seed: Int,
    road_width: Float = 0.25,
    size_x: Float = 5,
    size_y: Float = 5,
    density: Float = 10,
    building_size_min: Vector = (0.1, 0.1, 0.2),
    building_size_max: Vector = (0.3, 0.3, 1),
):
    # Road mesh
    yield geometry.curve_to_mesh(
        profile_curve=curve_line(
            start=combine_xyz(x=road_width * -0.5), end=combine_xyz(x=road_width * 0.5)
        )
    )
    # Building points
    building_points = (
        grid(size_x=size_x, size_y=size_y)
        .distribute_points_on_faces(density=density, seed=seed)
        .points
    )
    road_points = geometry.curve_to_points(mode=CurveToPoints.Mode.EVALUATED).points
    # Delete points within the curve
    building_points = building_points.delete_geometry(
        domain=DeleteGeometry.Domain.POINT,
        selection=geometry_proximity(
            target_element=GeometryProximity.TargetElement.POINTS,
            target=road_points,
            source_position=position(),
        ).distance
        < road_width,
    )
    # Building instances
    yield building_points.instance_on_points(
        instance=cube().transform(translation=(0, 0, 0.5)),
        scale=random_value(
            data_type=RandomValue.DataType.FLOAT_VECTOR,
            min=building_size_min,
            max=building_size_max,
            seed=seed,
        ),
    )