Overview

Being able to load and manipulate data is the first step towards rendering a scene. simple-3dviz implements various Renderables such as meshes, point clouds, lines e.t.c that can be used for configuring different types of input.

Creating Meshes

simple-3dviz allows you to configure a Mesh from a file, a set of vertices and normals, a voxel grid or a set of geometric primitives, which we model using superquadric surfaces.

Loading meshes from files can be easily done using the Mesh.from_file() method. Note that the supported mesh file formats are .obj, .off, .stl and .ply.

>>> from simple_3dviz import Mesh
>>> m = Mesh.from_file("models/sheep.stl")

# It is possible to load a mesh with any color you like
>>> m = Mesh.from_file("models/sheep.stl", color=(1, 1, 0))
# And with transparency
>>> m = Mesh.from_file("models/sheep.stl", color=(0.3, 1, 0.3, 0.6))

# It is also possible to manipulate the position of a mesh in a scene by
# properly adjusting its offset
>>> m = Mesh.from_file("models/sheep.stl", color=(0.8, 0.8, 0.8))
>>> m2 = Mesh.from_file("models/pig.stl", color=(0.8, 0.38, 0.58))
>>> m2.offset = (45, 0, 0)
Gray Sheep Yellow Sheep Green Sheep Sheep and Pig

You can also create a Mesh from a set of 3D points using the Mesh.from_xyz() method, from a set of vertices and faces using the Mesh.from_faces() method or using a set of vertices and normals.

>>> from simple_3dviz import Mesh

# Let us visualize a spherical harmonic as a surface similar to
# https://docs.enthought.com/mayavi/mayavi/mlab.html
>>> import numpy as np
>>> dphi, dtheta = np.pi/250.0, np.pi/250.0
>>> [phi, theta] = np.mgrid[0:np.pi+dphi*1.5:dphi, 0:2*np.pi+dtheta*1.5:dtheta]
>>> m0 = 4; m1 = 3; m2 = 2; m3 = 3; m4 = 6; m5 = 2; m6 = 6; m7 = 4;
>>> r = np.sin(m0 * phi)**m1 + np.cos(m2 * phi)**m3
>>> r = r + np.sin(m4 * theta)**m5 + np.cos(m6 * theta)**m7
>>> x = r * np.sin(phi) * np.cos(theta)
>>> y = r * np.cos(phi)
>>> z = r * np.sin(phi) * np.sin(theta)

>>> m = Mesh.from_xyz(x, y, z)

# It is also possible to load a mesh with a colormap
>>> import matplotlib.pyplot as plt
>>> m = Mesh.from_xyz(x, y, z, colormap=plt.cm.jet)

# We visualize a cone defined using vertices and faces, similar to
# https://docs.enthought.com/mayavi/mayavi/auto/mlab_helper_functions.html#points3d
>>> t = np.linspace(-np.pi, np.pi, 16)
>>> z = np.exp(1j * t)
>>> x = z.real.copy()
>>> y = z.imag.copy()
>>> z = np.zeros_like(x)
>>> 
>>> faces = [(0, i, i + 1) for i in range(1, 16)]
>>> x = np.r_[0, x]
>>> y = np.r_[0, y]
>>> z = np.r_[1, z]
>>> vertices = np.stack([x, y, z]).T
>>> colors = np.ones((len(points), 3))*[1.0, 0.0, 0.0]
>>> m = Mesh.from_faces(vertices, faces, colors=colors)
Spherical Harmonic Spherical Harmonic Viridis Red Cone

Creating Textured Meshes

simple-3dviz also supports rendering meshes with textures. To load a mesh with a texture/material you can use the TexturedMesh class similar to the way you would use Mesh.

>>> from simple_3dviz import TexturedMesh
>>> from simple_3dviz.behaviours.misc import LightToCamera
>>> from simple_3dviz.window import show

# The model together with its material files is downloaded for free from
# https://www.turbosquid.com/3d-models/african-elephant-obj-free/1126601
>>> m = TexturedMesh.from_file("path/to/elefante.obj")
>>> show(
...     m,
...     behaviours=[LightToCamera()],
...     camera_position=(8, 8, 8),
...     up_vector=(0, 1, 0)
... )

# You can also set the material after loading the model
>>> from simple_3dviz.renderables.textured_mesh import Material
>>> mtl = Material.with_texture_image(
...     "path/to/elefantefull.png",
...     ambient=(0.4, 0.4, 0.4),
...     diffuse=(0.4, 0.4, 0.4),
...     specular=(0.1, 0.1, 0.1),
...     Ns=2
... )
>>> m.material = mtl
>>> show(
...     m,
...     behaviours=[LightToCamera()],
...     camera_position=(8, 8, 8),
...     up_vector=(0, 1, 0)
... )
Original Elefante model Elefante model with custom material

Note that is is also possible to render textured meshes with multiple materials. You can again load a textured mesh with multiple materials using the TexturedMesh class as we did previously. In the following example you can see how you can render a 3D mesh with multiple materials, render it from various view points and save the animation as a gif.

>>> from simple_3dviz.behaviours.io import SaveGif
>>> from simple_3dviz.behaviours.movements import CameraTrajectory
>>> from simple_3dviz.behaviours.trajectory import Circle
>>> from simple_3dviz.renderables import TexturedMesh
>>> from simple_3dviz.utils import render, save_frame

# The 3D model together with its material is from ShapeNet
>>> rr1 = TexturedMesh.from_file("/tmp/motorbikes/motorbike_1.obj")
# Transfer the mesh to the unit cube
>>> rr1.to_unit_cube()
# Scale and transfer the mesh within a unit cube
>>> render(
...    rr1,
...    [
...        CameraTrajectory(
...            Circle((0, 0, 0), (0.0, 0.60, 1.4), (0, -1, 0)),
...            speed=1/180
...        ),
...        SaveGif("/tmp/motorbike_1.gif")
...    ],
...    up_vector=(0, 1, 0),
...    size=(1200, 1200),
...    camera_position=(0.0, 0.60, 1.4),
...    light=(0,5,0)
... )

# The 3D model together with its material is from ShapeNet
>>> rr2 = TexturedMesh.from_file("/tmp/motorbikes/motorbike_2.obj")
# Render the scene and save the animation as a gif
>>> render(
...    rr2,
...    [
...        CameraTrajectory(
...            Circle((0, 0, 0), (0.0, 0.4, 1.0), (0, -1, 0)),
...            speed=1/180
...        ),
...        SaveGif("/tmp/motorbike_2.gif")
...    ],
...    180,
...    camera_position=(0.0, 0.4, 1.0),
...    light=(0,)*3,
...    background=(1,)*4,
...    up_vector=(0, 1, 0)
...)

# It is possible to turn off the culling of faces that are pointing away
# from the camera, which by default is set to true
>>> if hasattr(rr2, "renderables"):
...     for r in rr2.renderables:
...         r.cull_back_face = False
... else:
...     rr2.cull_back_face = False

# Render the scene and save the animation as a gif
>>> render(
...    rr2,
...    [
...        CameraTrajectory(
...            Circle((0, 0, 0), (0.0, 0.4, 1.0), (0, -1, 0)),
...            speed=1/180
...        ),
...        SaveGif("/tmp/motorbike_3.gif")
...    ],
...    180,
...    camera_position=(0.0, 0.4, 1.0),
...    light=(0,)*3,
...    background=(1,)*4,
...    up_vector=(0, 1, 0)
...)
Motorbike model from ShapeNet Motorbike model from ShapeNet Motorbike model from ShapeNet

Creating Voxel Grids

It is possible to create a Mesh containing a voxel grid directly from an array of 3D values, where true values indicate the voxels to be filled using the Mesh.from_voxel_grid() method.

>>> from simple_3dviz import Mesh
>>> from simple_3dviz.window import show

# We create a NxNxN voxel grid with True values if the voxel is inside a heart
# object and False otherwise. We implement the Taubin's heart surface as
# described
# https://www.wolframalpha.com/input/?i=taubin%27s+heart+surface
>>> import numpy as np
>>> N = 64
>>> x = np.linspace(-1.3, 1.3, N)
>>> y = np.linspace(-1.3, 1.3, N)
>>> z = np.linspace(-1.3, 1.3, N)
>>> x, y, z = np.meshgrid(x, y, z)
>>> voxels = (2*x**2 + y**2 + z**2-1)**3 - (1/10) * x**2*z**3 - y**2*z**3 < 0
>>> m = Mesh.from_voxel_grid(voxels, colors=(0.8, 0, 0))
>>> show(m)

# It is also possible to visualize the voxel grid with boundaries by creating
# a Lines renderable object
>>> from simple_3dviz import Lines
>>> l = Lines.from_voxel_grid(voxels, colors=(0, 0, 0.), width=0.01)
>>> show([m, l])
Voxelized heart Voxelized heart with lines

Creating Point clouds

simple-3dviz also allows you to create and manipulate a point cloud directly from a numpy array using the Spherecloud renderable.

>>> from simple_3dviz import Spherecloud
>>> import numpy as np

>>> def fexp(x, p):
...     return np.sign(x)*(np.abs(x)**p)

# Define the parametric function of superquadric surfaces
>>> def sq_surface(a1, a2, a3, e1, e2, eta, omega):
...     x = a1 * fexp(np.cos(eta), e1) * fexp(np.cos(omega), e2)
...     y = a2 * fexp(np.cos(eta), e1) * fexp(np.sin(omega), e2)
...     z = a3 * fexp(np.sin(eta), e1)
...     return x, y, z

# Sample points on the surface of the supequadric
>>> eta = np.linspace(-np.pi/2, np.pi/2, 100, endpoint=True)
>>> omega = np.linspace(-np.pi, np.pi, 100, endpoint=True)
>>> eta, omega = np.meshgrid(eta, omega)
>>> x, y, z = sq_surface(a1=0.4, a2=0.4, a3=0.4, e1=1.0, e2=1.0, eta=eta, omega=omega)
>>> centers = np.stack([x, y, z]).reshape(3, -1).T
>>> s = Spherecloud(centers[::30])

# It is possible to modify the color and the size of the rendered point cloud
# by properly setting the colors and sizes arguments.
>>> x = np.linspace(-0.5, 0.5, num=5)
>>> centers = np.array(np.meshgrid(x, x, x)).reshape(3, -1).T
>>> colors = np.ones((centers.shape[0], 4))
>>> colors[centers[:, 1] == -0.5] = [1, 1, 0, 1]
>>> colors[centers[:, 1] == -0.25] = [0, 1, 1, 1]
>>> colors[centers[:, 1] == 0] = [0.2, 0.8, 0.3, 1]
>>> colors[centers[:, 1] == 0.25] = [1.0, 0.41, 0.7, 1]
>>> colors[centers[:, 1] == 0.5] = [0.8, 0.2, 0, 1]
>>> sizes = np.ones(centers.shape[0])*0.05
>>> s = Spherecloud(centers, colors=colors, sizes=sizes)
Spherecloud simple Spherecloud colored Spherecloud squirrel Spherecloud plane

Creating Geometric Primitives

We use superquadric surfaces for modelling geometric primitives. Superquadrics are a parametric family of shapes that can represent various shapes using the same continuous space. They can be fully described with just 11 parameters, 2 for the shape, 3 for the size and 6 for the pose in the 3d space. With simple-3dviz, you can easily create a Mesh containing a set of primitives using the Mesh.from_superquadrics() function.

>>> import numpy as np
>>> from simple_3dviz import Mesh

# We will render various superquadrics with different shape parameters e1 and
# e2
>> N = 7

# SQs shapes
>>> e2 = np.linspace(0.1, 1.9, N, endpoint=True)
>>> e1 = np.linspace(0.1, 1.9, N, endpoint=True)
>>> epsilon_1, epsilon_2 = np.meshgrid(e1, e2)
>>> epsilon = np.stack([epsilon_1, epsilon_2]).reshape(2, -1).T

# SQs sizes
>>> alpha = np.ones((epsilons.shape[0], 3))
# SQs translations
>>> s = np.ceil(N*2.5 / 2)
>>> x = np.linspace(-s, s, N)
>>> y = np.linspace(-s, s, N)
>>> z = np.array([0])
>>> X, Y, Z = np.meshgrid(x, y, z)
>>> translation = np.stack([X, Y, Z]).reshape(3, -1).T

# SQs rotations
>>> rotation = np.eye(3)[np.newaxis] * np.ones((len(epsilons), 1, 1))

>>> colors = np.array([[1., 0, 0, 1],
...                    [0, 1, 1, 1]])[np.random.randint(0, 2, size=epsilons.shape[0])]

>>> m = Mesh.from_superquadrics(alpha, epsilon, translation, rotation, colors)
Superquadrics

Creating Lines

simple-3dviz allows you to create lines directly from a numpy array that contains the line segments using the Lines object. Similar to the above, it is possible to adjust both the size and the color of each line segment.

>>> from simple_3dviz import Lines

# Create the line segments that yield a cube
>>> l = Lines([
            [-0.6, -0.6, -0.6], 
            [-0.6, -0.6,  0.6],
            [-0.6, -0.6,  0.6], 
            [-0.6,  0.6,  0.6],
            [-0.6,  0.6,  0.6], 
            [-0.6,  0.6, -0.6],
            [-0.6,  0.6, -0.6], 
            [-0.6, -0.6, -0.6],
            [ 0.6, -0.6, -0.6], 
            [ 0.6, -0.6,  0.6],
            [ 0.6, -0.6,  0.6], 
            [ 0.6,  0.6,  0.6],
            [ 0.6,  0.6,  0.6], 
            [ 0.6,  0.6, -0.6],
            [ 0.6,  0.6, -0.6], 
            [ 0.6, -0.6, -0.6],
            [ 0.6,  0.6,  0.6],
            [-0.6,  0.6,  0.6],
            [ 0.6,  0.6, -0.6],
            [-0.6,  0.6, -0.6],
            [ 0.6, -0.6,  0.6],
            [-0.6, -0.6,  0.6],
            [ 0.6, -0.6, -0.6],
            [-0.6, -0.6, -0.6],
        ], (0.1, 0.1, 0.1, 1.0), width=0.02)

# Create axes
>>> l = Lines([
            [0.0, 0.0, 0.0],
            [0.6, 0.0, 0.0],
            [0.0, 0.0, 0.0],
            [0.0, 0.6, 0.0],
            [0.0, 0.0, 0.0],
            [0.0, 0.0, 0.6]
        ],
        colors = np.array([
            [1.0, 0.0, 0.0, 1.0],
            [1.0, 0.0, 0.0, 1.0],
            [0.0, 1.0, 0.0, 1.0],
            [0.0, 1.0, 0.0, 1.0],
            [0.0, 0.0, 1.0, 1.0],
            [0.0, 0.0, 1.0, 1.0]
        ]), width=0.02)
Cube with lines Axes with lines Primitives with lines