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!
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.
- Go to
Code > Download ZIP
above and then in blender go toEdit > Preferences > Add-ons > install
and 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 plots
and click on the tick-box. - You should now be able to run
import blender_plots as bplt
in the python console.
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.
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 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")
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)
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")
sinusoids_animated.mp4
For animated surface plots the input shape should be TxMxNx3
.
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.
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.
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),
)
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,
)
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
)