blender-plots
blender-plots copied to clipboard
Python library for making 3D plots with blender
blender-plots
Ever wonder if you could use Blender for plotting, like you would with Matplotlib? With blender-plots you can!
![]() |
![]() |
Blender can indeed be a great tool for visualization, but if you're starting out with numpy arrays getting even a simple plot with colors requires a lot of digging through the python API and wrangling node trees. The goal of this library/addon is to automate all of that and make the visualization process as painless as possible for someone familiar with matplotlib-like libraries.
At the moment this readme acts as the documentation. The easiest way to get started is to have a look at the examples below and in examples.ipynb. Feel free to open an issue if anything is unclear or doesn't work as intended, feedback and suggestions are appreciated!
Installation
Option 1: Install with pip (recommended)
Run the following command in a terminal window:
[path_to_blender]/[version_number]/python/bin/python3.10 -m pip install blender_plots
This will download the library to [path_to_blender]/[version_number]/python/lib/python3.10/site-packages/blender-plots.
If you're using blender_notebook as described below you can instead pip install to the virtual environment to keep the Blender python environment clean. However with this method the library won't be available when lauching Blender without the notebook.
Option 2: Install as addon
- Go to
Code > Download ZIPabove and then in blender go toEdit > Preferences > Add-ons > installand select the downloaded file (or simply git-clone this repo to the blender addons folder). - Go to the addons panel in blender, search for
blender plotsand click on the tick-box. - You should now be able to run
import blender_plots as bpltin the python console.
Set up blender notebook (optional)
Since the built-in code editor isn't great I recommend using jupyter notebooks with a blender kernel for script heavy use cases.
In a virtual environment, run:
python -m pip install blender_notebook ipykernel
blender_notebook install --blender-exec [path_to_blender]/blender --kernel-name blender
You should then be able to select blender as kernel in your preferred notebook editor.
Examples
Scatterplots
Scatterplots can be created with bplt.Scatter which expects three arrays x, y, z with the same length N
containing coordinates to plot (or equivalently a single Nx3 array as the first argument). Color can also be set using
the color= argument, which expects aNx3 or Nx4 numpy array with RGB or RGBA color values. Passing in a single RGB
or RGBA value sets the same color for all points.
If the output is all gray it's probably because the rendering mode is set to Solid. In that case make sure to change it to Material Preview or Rendered by clicking on one of the sphere icons in the top right corner of the 3D view.
import numpy as np
import blender_plots as bplt
n, l = 150, 100
x, y = np.meshgrid(np.linspace(0, l, n), np.linspace(0, l, n))
x, y = x.ravel(), y.ravel()
z = np.sin(2*np.pi * x / l)*np.sin(2*np.pi * y / l) * 20
bplt.Scatter(x, y, z, color=(1, 0, 0), name="red")
z = np.sin(4*np.pi * x / l)*np.sin(4*np.pi * y / l) * 20 + 40
bplt.Scatter(x, y, z, color=(0, 0, 1), name="blue")

Surface plots
Surface plots can be created in the same way, except using MxNx3 arrays for x, y, z. Faces are then added between points neighbouring along the x and y axes. Colors and animation can be added in the same way as with scatterplots.
import numpy as np
import blender_plots as bplt
n, l = 150, 100
x, y = np.meshgrid(np.linspace(0, l, n), np.linspace(0, l, n))
z = np.sin(2*np.pi * x / l)*np.sin(2*np.pi * y / l) * 20
bplt.Surface(x, y, z, color=(1, 0, 0), name="red")
z = np.sin(4*np.pi * x / l)*np.sin(4*np.pi * y / l) * 20 + 40
bplt.Surface(x, y, z, color=(0, 0, 1), name="blue")

Arrow plots
Create an arrow plot by providing an Nx3 array of starting points and an Nx3 array of vectors representing the arrows.
import numpy as np
import blender_plots as bplt
import bpy
n, a, I = 25, 50, 100
bpy.ops.mesh.primitive_torus_add(major_radius=a, minor_radius=a / 100)
phis = np.linspace(0, 2 * np.pi, n)
thetas = np.linspace(0, 2 * np.pi, 1000)
def integrate_B(point):
return I * a * (np.array([
point[2] * np.cos(thetas),
-point[2] * np.sin(thetas),
-point[0] * np.cos(thetas) - point[1] * np.sin(thetas) + a
]) / np.linalg.norm(np.array([
point[0] - a * np.cos(thetas),
point[1] - a * np.sin(thetas),
point[2] + np.zeros_like(thetas),
]), axis=0) ** 3).sum(axis=1) * (thetas[1] - thetas[0])
phis = np.linspace(0, 2 * np.pi, n)
thetas = np.linspace(0, 2 * np.pi, 100)
zeros = np.zeros(n)
for r in [a / 2, 3 * a / 4, a]:
x, y, z = (a + r * np.cos(phis)), zeros, r * np.sin(phis)
points = np.array([x, y, z]).T
B = np.apply_along_axis(integrate_B, 1, points)
bplt.Arrows(points, B, color=(1, 0, 0), name=f"B_{r}", radius=.3, radius_ratio=3)
phis = np.linspace(0, 2 * np.pi, 13)
x, y, z = 1.01 * a * np.cos(phis), 1.01 * a * np.sin(phis), np.zeros(phis.shape)
points = np.array([x, y, z]).T
current_directions = np.array([-y, x, z]).T
bplt.Arrows(points, current_directions * .5, color=(0, 0, 1), name=f"I_directions", radius=.3, radius_ratio=3)
bplt.Scatter(points, color=(0, 0, 1), name=f"I_points", marker_type='ico_spheres', radius=1, subdivisions=3)

Animations
To get an animated plot, just pass in x, y, z as TxN arrays instead (or TxNx3 as the first argument):
import numpy as np
import blender_plots as bplt
n, l, T = 150, 100, 100
t, x, y = np.meshgrid(np.arange(0, T), np.linspace(0, l, n), np.linspace(0, l, n), indexing='ij')
t, x, y = t.reshape((T, -1)), x.reshape((T, -1)), y.reshape((T, -1))
z = np.sin(2*np.pi * x / l) * np.sin(2*np.pi * y / l) * np.sin(2*np.pi * t / T) * 20
bplt.Scatter(x, y, z, color=(1, 0, 0), name="red")
z = np.sin(4*np.pi * x / l) * np.sin(4*np.pi * y / l) * np.sin(8*np.pi * t / T) * 20 + 40
bplt.Scatter(x, y, z, color=(0, 0, 1), name="blue")
https://user-images.githubusercontent.com/12471058/175827154-f2788971-78d3-4778-937a-5d0ff30af7fd.mp4
For animated surface plots the input shape should be TxMxNx3.
Visualizing point clouds
Since all heavy operations are done through numpy arrays or blender nodes it's possible to visualize large point clouds with minimal overhead. For example, Here is one with 1M points:
import numpy as np
import blender_plots as bplt
points = np.loadtxt("/home/linus/Downloads/tikal-guatemala-point-cloud/source/fovea_tikal_guatemala_pcloud.asc")
scatter = bplt.Scatter(points[:, :3] - points[0, :3], color=points[:, 3:]/255, size=(0.3,0.3,0.3))

You can find the
model here
(select .asc format). Original source: OpenHeritage,
license: CC Attribution-NonCommercial-ShareAlikeCC.
Marker options
You can swap from the cube to any other mesh primitive using the marker_type argument. In blender 3.1 the options are
cones,
cubes,
cylinders,
grids,
ico_spheres,
circles,
lines or
uv_spheres.

Each marker type can further be configured by passing in node settings as parameter arguments. For example from
the cone node
docs we can see that it has the parameters Radius Top and Radius Bottom, these can be set directly by
passing radius_top=...
and radius_bottom=... to bplt.Scatter:
import numpy as np
import blender_plots as bplt
n = int(1e2)
scatter = bplt.Scatter(
np.random.rand(n, 3)*50,
color=np.random.rand(n, 3),
marker_type="cones",
radius_bottom=1,
radius_top=3,
randomize_rotation=True
)

Similarly, the cube node
has a vector-valued Size parameter:
import numpy as np
import blender_plots as bplt
n = int(1e2)
bplt.Scatter(
np.random.rand(n, 3)*50,
color=np.random.rand(n, 3),
size=(5, 1, 1),
randomize_rotation=True
)

This is achieved by automatically converting input arguments to geometry node properties. See blender_utils.py for more details.
Rotating markers
Each marker can be assigned a rotation using the argument marker_rotation=..., similarly to the color argument it
supports passing a single value for all points, one for each point, or one for each point and timestamp.
The supported formats are XYZ euler angles in radians (by passing an Nx3 or NxTx3 array) or rotation matrices
(by passing a Nx3x3 or NxTx3x3 array). As shown in the previous examples passing randomize_rotation=True assigns a
random rotation to each marker.
import numpy as np
import blender_plots as bplt
def get_rotaitons_facing_point(origin, points):
n_points = len(points)
d = (origin - points) / np.linalg.norm(origin - points, axis=-1)[:, None]
R = np.zeros((n_points, 3, 3))
R[..., -1] = d
R[..., 0] = np.cross(d, np.random.randn(n_points, 3))
R[..., 0] /= np.linalg.norm(R[..., 0], axis=-1)[..., None]
R[..., 1] = np.cross(R[..., 2], R[..., 0])
return R
n = 5000
points = np.random.randn(n, 3) * 20
rots = get_rotaitons_facing_point(np.zeros(3), points)
s = bplt.Scatter(
points,
marker_rotation=rots,
color=np.array([[1.0, 0.1094, 0.0], [0.0, 0.1301, 1.0]])[np.random.randint(2, size=n)],
size=(1, 1, 5),
)

Custom mesh as marker
You can also use an existing mesh by passing it to marker_type=...:
import numpy as np
from colorsys import hls_to_rgb
import bpy
import blender_plots as bplt
bpy.ops.mesh.primitive_monkey_add()
monkey = bpy.context.active_object
monkey.hide_viewport = True
monkey.hide_render = True
n = int(.5e2)
scatter = bplt.Scatter(
50 * np.cos(np.linspace(0, 1, n)*np.pi*4),
50 * np.sin(np.linspace(0, 1, n)*np.pi*4),
50 * np.linspace(0, 1, n),
color=np.array([hls_to_rgb(2/3 * i/(n-1), 0.5, 1) for i in range(n)]),
marker_type=monkey,
radius_bottom=1,
radius_top=3,
marker_scale=[5]*3,
marker_rotation=np.array([np.zeros(n), np.zeros(n), np.pi/2 + np.linspace(0, 4 * np.pi, n)]).T,
)

Sphere markers
You can get perfect spheres as markers by passing in marker_type="spheres". Though note that these are only visible in
the rendered view and with the rendering engine set to cycles
import numpy as np
import blender_plots as bplt
n = int(1e2)
bplt.Scatter(
np.random.rand(n, 3)*50,
color=np.random.rand(n, 3),
marker_type="spheres",
radius=1.5
)


