Module simple_3dviz.renderables.mesh

Expand source code
import numpy as np

from ..io import read_mesh_file
from ..io.voxels import read_binvox
from .base import Renderable

from pyrr import Matrix44, matrix44


class MeshBase(Renderable):
    """Abstract base class that implements functions commonly used by the
    subclasses."""
    def __init__(self, vertices, normals, offset=[0, 0, 0.]):
        self._vertices = np.asarray(vertices)
        self._normals = np.asarray(normals)
        self._model_matrix = np.eye(4).astype(np.float32)
        self._offset = np.asarray(offset).astype(np.float32)

        self._prog = None
        self._vbo = None
        self._vao = None

    @property
    def bbox(self):
        """The axis aligned bounding box of all the vertices as two
        3-dimensional arrays containing the minimum and maximum for each
        axis."""
        return [
            self._vertices.min(axis=0),
            self._vertices.max(axis=0)
        ]

    @property
    def model_matrix(self):
        """An affine transformation matrix (4x4) applied to the mesh before
        rendering. Can be changed to animate the mesh."""
        return self._model_matrix

    @model_matrix.setter
    def model_matrix(self, v):
        self._model_matrix = np.asarray(v).astype(np.float32)
        if self._prog:
            self._prog["local_model"].write(self._model_matrix.tobytes())

    def rotate_x(self, angle):
        """Helper function that multiplies the `model_matrix` with a rotation
        matrix around the x axis."""
        m = Matrix44.from_x_rotation(angle)
        self.model_matrix = m.dot(self.model_matrix)

    def rotate_y(self, angle):
        """Helper function that multiplies the `model_matrix` with a rotation
        matrix around the y axis."""
        m = Matrix44.from_y_rotation(angle)
        self.model_matrix = m.dot(self.model_matrix)

    def rotate_z(self, angle):
        """Helper function that multiplies the `model_matrix` with a rotation
        matrix around the z axis."""
        m = Matrix44.from_z_rotation(angle)
        self.model_matrix = m.dot(self.model_matrix)

    def rotate_axis(self, axis, angle):
        """Helper function that multiplies the `model_matrix` with a rotation
        matrix around the passed in axis."""
        m = matrix44.create_from_axis_rotation(axis, angle)
        self.model_matrix = m.dot(self.model_matrix)

    @property
    def offset(self):
        """A translation vector for the mesh vertices."""
        return self._offset

    @offset.setter
    def offset(self, v):
        self._offset = np.asarray(v).astype(np.float32)
        if self._prog:
            self._prog["offset"].write(self._offset.tobytes())

    def scale(self, s):
        """Multiply all the vertices with a number s."""
        self._vertices *= s
        self._update_vbo()

    def affine_transform(self, R=np.eye(3), t=np.zeros(3)):
        """Rotate and translate the vertices and then update the gpu buffer.

        Given the vertices v \in R^{Nx3} this function implements

            v' = v @ R + t

        Arguments
        ---------
            R: array (3, 3), the 3x3 rotation matrix
            t: array (3,), the translation vector
        """
        self._vertices = self._vertices.dot(R) + t
        self._update_vbo()

    def to_unit_cube(self):
        """Transform the mesh such that it fits in the 0 centered unit cube."""
        bbox = self.bbox
        dims = bbox[1] - bbox[0]
        self._vertices -= dims/2 + bbox[0]
        self._vertices /= dims.max()
        self._update_vbo()

    def release(self):
        self._prog.release()
        self._vbo.release()
        self._vao.release()

    def render(self):
        self._vao.render()

    def update_uniforms(self, uniforms):
        uniforms_list = self._get_uniforms_list()
        for k, v in uniforms:
            if k in uniforms_list:
                self._prog[k].write(v.tobytes())

    @staticmethod
    def _triangle_normals(triangles):
        triangles = triangles.reshape(-1, 3, 3)
        ba = triangles[:, 1] - triangles[:, 0]
        bc = triangles[:, 2] - triangles[:, 1]
        return np.cross(ba, bc, axis=-1)

    def _update_vbo(self):
        """Update the vertex buffer object because one of the values has
        changed (vertices, normals, etc)."""
        raise NotImplementedError()


class Mesh(MeshBase):
    """A mesh is a collection of triangles with normals and colors.

    Arguments
    ---------
        vertices: array-like, the vertices of the triangles. Each triangle
                  should be given on its own even if vertices are shared.
        normals: array-like, per vertex normal vectors
        colors: array-like, per vertex color as (r, g, b) floats or
                (r, g, b, a) floats. If one color is given then it is assumed
                to be for all vertices.
        offset: A translation vector for all the vertices. It can be changed
                after construction to animate the object together with the
                `model_matrix` property.
    """
    def __init__(self, vertices, normals, colors, offset=[0, 0, 0.]):
        super(Mesh, self).__init__(vertices, normals, offset)

        self._colors = np.asarray(colors)

        N = len(self._vertices)
        if len(self._colors.shape) == 1:
            if self._colors.size == 3:
                self._colors = np.array(self._colors.tolist() + [1])
            self._colors = self._colors[np.newaxis].repeat(N, axis=0)
        elif self._colors.shape[1] == 3:
            self._colors = np.hstack([self._colors, np.ones((N, 1))])

    def init(self, ctx):
        self._prog = ctx.program(
            vertex_shader="""
                #version 330

                uniform mat4 mvp;
                uniform mat4 local_model;
                uniform vec3 offset;
                in vec3 in_vert;
                in vec3 in_norm;
                in vec4 in_color;
                out vec3 v_vert;
                out vec3 v_norm;
                out vec4 v_color;

                void main() {
                    v_color = in_color;

                    // Compute the position of the vertex
                    vec4 t_position = vec4(in_vert, 1.0);
                    t_position = local_model * t_position;
                    t_position = t_position + vec4(offset, 0.);
                    v_vert = t_position.xyz / t_position.w;
                    // TODO: We should probably use the global model matrix as
                    //       well but it is left for the future 
                    t_position = mvp * t_position;
                    gl_Position = t_position;

                    // Compute the normal of the vertex
                    vec4 t_normal = vec4(in_norm, 1.0);
                    t_normal = local_model * t_normal;
                    v_norm = t_normal.xyz / t_normal.w;
                }
            """,
            fragment_shader="""
                #version 330

                uniform vec3 light;
                in vec3 v_vert;
                in vec3 v_norm;
                in vec4 v_color;

                out vec4 f_color;

                void main() {
                    float lum = dot(normalize(v_norm), normalize(v_vert - light));
                    lum = acos(lum) / 3.14159265;
                    lum = clamp(lum, 0.0, 1.0);

                    f_color = vec4(v_color.xyz * lum, v_color.w);
                }
            """
        )
        self._vbo = ctx.buffer(np.hstack([
            self._vertices,
            self._normals,
            self._colors
        ]).astype(np.float32).tobytes())
        self._vao = ctx.simple_vertex_array(
            self._prog,
            self._vbo,
            "in_vert", "in_norm", "in_color"
        )
        self.model_matrix = self._model_matrix
        self.offset = self._offset

    def _get_uniforms_list(self):
        """Return the used uniforms to fetch from the scene."""
        return ["light", "mvp"]

    def _update_vbo(self):
        """Write in the vertex buffer object the vertices, normals and
        colors."""
        if self._vbo is not None:
            self._vbo.write(np.hstack([
                self._vertices, self._normals, self._colors
            ]).astype(np.float32).tobytes())

    def sort_triangles(self, point):
        """Sort the triangles such that the first is furthest from `point` and
        the last is the closest to `point`.

        It is used so that transparency works properly in OpenGL.
        """
        vertices = self._vertices.reshape(-1, 3, 3)
        normals = self._normals.reshape(-1, 9)
        colors = self._colors.reshape(-1, 12)

        centers = vertices.mean(-2)
        d = ((np.asarray(point).reshape(1, 3) - centers)**2).sum(-1)
        alpha = (colors[:, ::4].mean(-1)<1).astype(np.float32) * 1000
        idxs = np.argsort(d+alpha)[::-1]

        self._vertices = vertices[idxs].reshape(-1, 3)
        self._normals = normals[idxs].reshape(-1, 3)
        self._colors = colors[idxs].reshape(-1, 4)
        self._update_vbo()

    def scale(self, s):
        """Multiply all the vertices with a number s."""
        self._vertices *= s
        self._update_vbo()

    def translate(self, t):
        """Translate all the vertices with a vector t."""
        self._vertices += t
        self._update_vbo()

    def to_unit_cube(self):
        """Transform the mesh such that it fits in the 0 centered unit cube."""
        bbox = self.bbox
        dims = bbox[1] - bbox[0]
        self._vertices -= dims/2 + bbox[0]
        self._vertices /= dims.max()
        self._update_vbo()

    @classmethod
    def from_file(cls, filepath, color=(0.3, 0.3, 0.3), ext=None):
        """Read the mesh from a file.

        Arguments
        ---------
            filepath: Path to file or file object containing the mesh
            color: A default color to load if the information is not provided
                   in the file
            ext: The file extension (including the dot) if `filepath` is an
                 object
        """
        # Read the mesh
        mesh = read_mesh_file(filepath, ext=ext)

        # Extract the triangles
        vertices = mesh.vertices

        # Set a normal per triangle vertex
        try:
            normals = mesh.normals
        except NotImplementedError:
            normals = np.repeat(Mesh._triangle_normals(vertices), 3, axis=0)

        # Set a color per triangle vertex
        try:
            colors = mesh.colors
        except NotImplementedError:
            colors = np.ones((len(vertices), 1)) * color

        return cls(vertices, normals, colors)

    @classmethod
    def from_xyz(cls, X, Y, Z, colormap=None):
        X, Y, Z = list(map(np.asarray, [X, Y, Z]))
        def gray(x):
            return np.ones((x.shape[0], 3))*x[:, np.newaxis]

        def normalize(x):
            xmin = x.min()
            xmax = x.max()
            return 2*(x-xmin)/(xmax-xmin) - 1

        def idx(i, j, x):
            return i*x.shape[1] + j

        # Normalize dimensions in [-1, 1]
        x = normalize(X)
        y = normalize(Y)
        z = normalize(Z)

        # Create faces by triangulating each quad
        faces = []
        for i in range(x.shape[0]-1):
            for j in range(y.shape[1]-1):
                # i, j; i, j+1; i+1; j+1
                # i, j; i+1, j; i+1; j+1
                faces.extend([
                    idx(i+1, j+1, x),
                    idx(i, j+1, x),
                    idx(i, j, x),
                    idx(i+1, j, x),
                    idx(i+1, j+1, x),
                    idx(i, j, x)
                ])

        vertices = np.vstack([x.ravel(), y.ravel(), z.ravel()]).T[faces]
        colors = (
            colormap(Z.ravel()[faces])
            if colormap else gray(z.ravel()[faces])
        )
        normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)

        return cls(vertices, normals, colors)

    @classmethod
    def from_faces(cls, vertices, faces, colors):
        vertices, faces, colors = list(map(
            np.asarray,
            [vertices, faces, colors]
        ))
        vertices = vertices[faces].reshape(-1, 3)
        normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)
        if len(colors.shape) != 1:
            colors = colors[faces].reshape(-1, 3)

        return cls(vertices, normals, colors)

    @classmethod
    def from_boxes(cls, centers, sizes, colors):
        """Create boxes.

        Arguments
        ---------
            centers: Array of 3 dimensional centers
            sizes: Array of 3 sizes per box that give, half the width, half the
                   depth, half the height
            colors: tuple for all boxes or array of colors per box
        """
        box = np.array([[-1, -1,  1],
                        [ 1, -1,  1],
                        [ 1,  1,  1],
                        [-1, -1,  1],
                        [ 1,  1,  1],
                        [-1,  1,  1],
                        [-1,  1, -1],
                        [ 1,  1,  1],
                        [-1,  1,  1],
                        [-1,  1, -1],
                        [ 1,  1, -1],
                        [ 1,  1,  1],
                        [-1,  1, -1],
                        [-1, -1,  1],
                        [-1,  1,  1],
                        [-1,  1, -1],
                        [-1, -1,  1],
                        [-1, -1, -1],
                        [ 1, -1, -1],
                        [ 1, -1,  1],
                        [ 1,  1,  1],
                        [ 1, -1, -1],
                        [ 1,  1, -1],
                        [ 1,  1,  1],
                        [ 1, -1, -1],
                        [-1, -1,  1],
                        [ 1, -1,  1],
                        [ 1, -1, -1],
                        [-1, -1,  1],
                        [-1, -1, -1],
                        [ 1, -1, -1],
                        [-1,  1, -1],
                        [ 1,  1, -1],
                        [ 1, -1, -1],
                        [-1,  1, -1],
                        [-1, -1, -1]]).astype(np.float32)

        normals = np.array([[ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0,  0, -1],
                            [ 0,  0, -1],
                            [ 0,  0, -1],
                            [ 0,  0, -1],
                            [ 0,  0, -1],
                            [ 0,  0, -1]]).astype(np.float32)

        centers, sizes, colors = list(map(
            np.asarray,
            [centers, sizes, colors]
        ))

        assert len(centers.shape) == 2 and centers.shape[1] == 3
        if len(sizes.shape) == 1:
            sizes = sizes[np.newaxis].repeat(len(centers), axis=0)
        vertices = centers[:, np.newaxis]+sizes[:, np.newaxis]*box[np.newaxis]
        vertices = vertices.reshape(-1, 3)
        normals = np.vstack([normals]*len(centers))

        if len(colors.shape) == 1:
            if colors.size < 4:
                colors = np.array(colors.tolist() + [1.]*(4-colors.size))
            colors = colors[np.newaxis].repeat(len(vertices), axis=0)
        if len(colors) != len(vertices) and len(colors) == len(centers):
            colors = np.repeat(colors, len(box), axis=0)

        return cls(vertices, normals, colors)

    @classmethod
    def from_voxel_grid(cls, voxels, sizes=None, colors=(0.3, 0.3, 0.3),
                        bbox=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]]):
        """ Create a voxel grid

        Arguments
        ---------
            voxels: Array of 3D values, with truthy values indicating which
                    voxels to fill
            colors: The colors of the voxels. If colors is a vector then
                    it is the same for all voxels. If it is a 4 dimensional
                    tensor then a color per voxel is assumed.
        """
        # Make sure voxels, colors and bbox are arrays
        voxels, colors, bbox = list(map(np.asarray, [voxels, colors, bbox]))

        # Ensure that the voxel grid is indeed a 3D grid
        assert len(voxels.shape) == 3
        M, N, K = voxels.shape

        # Clean and standardize the sizes
        if sizes is None:
            sizes = (bbox[1]-bbox[0]) * 0.48 / [M, N, K]
        else:
            sizes = np.asarray(sizes)

        # Convert the indices to center coordinates
        x, y, z = np.indices((M, N, K)).astype(np.float32)
        x = x / M * (bbox[1][0] - bbox[0][0]) + bbox[0][0]
        y = y / N * (bbox[1][1] - bbox[0][1]) + bbox[0][1]
        z = z / K * (bbox[1][2] - bbox[0][2]) + bbox[0][2]
        centers = np.vstack([x[voxels], y[voxels], z[voxels]]).T

        # Convert the sizes to per box sizes
        if len(sizes.shape) == 1:
            sizes = np.array([sizes for _ in range(len(centers))])
        elif len(sizes.shape) == 4:
            sizes = sizes[voxels]

        # Convert the colors to per box colors
        if len(colors.shape) == 1:
            colors = np.array([colors for _ in range(len(centers))])
        elif len(colors.shape) == 4:
            colors = colors[voxels]

        return cls.from_boxes(centers=centers, sizes=sizes, colors=colors)

    @classmethod
    def from_binvox(cls, binvoxfile, colors=(0.3, 0.3, 0.3)):
        """Create a voxel grid from a binvox file.

        For the format see https://patrickmin.com/binvox/binvox.html .
        
        Arguments
        ---------
            binvoxfile: str or file object that contains the voxelgrid data in
                        binvox format
            colors: The colors of the voxels to pass to from_voxel_grid().
        """
        voxelgrid, translation, scale = read_binvox(binvoxfile)
        bbox = np.array([[0., 0, 0], [1, 1, 1]]) * scale + translation

        return cls.from_voxel_grid(voxelgrid, colors=colors, bbox=bbox)

    @classmethod
    def from_superquadrics(cls, alpha, epsilon, translation, rotation, colors,
                           offset=[0, 0, 0.], vertex_count=10000):
        """Create Superquadrics.

        Arguments
        ---------
            alpha: Array of 3 sizes, along each axis
            epsilon: Array of 2 shapes, along each a
            translation: Array of 3 dimensional center
            rotation: Array of size 3x3 containing the rotations
            colors: Tuple for all sqs or array of colors per sq
        """
        def fexp(x, p):
            return np.sign(x)*(np.abs(x)**p)

        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

        # triangulate the sphere to be used with the SQs
        n = int(np.sqrt(vertex_count))
        eta = np.linspace(-np.pi/2, np.pi/2, n, endpoint=True)
        omega = np.linspace(-np.pi, np.pi, n, endpoint=True)
        triangles = []
        for o1, o2 in zip(np.roll(omega, 1), omega):
            triangles.extend([
                (eta[0], 0),
                (eta[1], o2),
                (eta[1], o1),
            ])
        for e in range(1, len(eta)-2):
            for o1, o2 in zip(np.roll(omega, 1), omega):
                triangles.extend([
                    (eta[e], o1),
                    (eta[e+1], o2),
                    (eta[e+1], o1),
                    (eta[e], o1),
                    (eta[e], o2),
                    (eta[e+1], o2),
                ])
        for o1, o2 in zip(np.roll(omega, 1), omega):
            triangles.extend([
                (eta[-1], 0),
                (eta[-2], o1),
                (eta[-2], o2),
            ])
        triangles = np.array(triangles)
        eta, omega = triangles[:, 0], triangles[:, 1]

        # collect the pretriangulated vertices of each SQ
        vertices = []
        a, e, t, R = list(map(
            np.asarray,
            [alpha, epsilon, translation, rotation]
        ))
        M, _ = a.shape  # number of superquadrics
        assert R.shape == (M, 3, 3)
        assert t.shape == (M, 3)
        for i in range(M):
            a1, a2, a3 = a[i]
            e1, e2 = e[i]
            x, y, z = sq_surface(a1, a2, a3, e1, e2, eta, omega)
            # Get points on the surface of each SQ
            V = np.stack([x, y, z], axis=-1)
            V = R[i].T.dot(V.T).T + t[i].reshape(1, 3)
            vertices.append(V)

        # Finalize the mesh
        vertices = np.vstack(vertices)
        normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)
        colors = np.asarray(colors)

        if len(colors.shape) == 1:
            if colors.size < 4:
                colors = np.array(colors.tolist() + [1.]*(4-colors.size))
            colors = colors[np.newaxis].repeat(len(vertices), axis=0)
        assert len(colors) == len(vertices) or len(colors) == M
        if len(colors) == M:
            colors = np.repeat(colors, len(vertices) // M, axis=0)

        return cls(vertices, normals, colors, offset)

Classes

class Mesh (vertices, normals, colors, offset=[0, 0, 0.0])

A mesh is a collection of triangles with normals and colors.

Arguments

vertices: array-like, the vertices of the triangles. Each triangle
          should be given on its own even if vertices are shared.
normals: array-like, per vertex normal vectors
colors: array-like, per vertex color as (r, g, b) floats or
        (r, g, b, a) floats. If one color is given then it is assumed
        to be for all vertices.
offset: A translation vector for all the vertices. It can be changed
        after construction to animate the object together with the
        `model_matrix` property.
Expand source code
class Mesh(MeshBase):
    """A mesh is a collection of triangles with normals and colors.

    Arguments
    ---------
        vertices: array-like, the vertices of the triangles. Each triangle
                  should be given on its own even if vertices are shared.
        normals: array-like, per vertex normal vectors
        colors: array-like, per vertex color as (r, g, b) floats or
                (r, g, b, a) floats. If one color is given then it is assumed
                to be for all vertices.
        offset: A translation vector for all the vertices. It can be changed
                after construction to animate the object together with the
                `model_matrix` property.
    """
    def __init__(self, vertices, normals, colors, offset=[0, 0, 0.]):
        super(Mesh, self).__init__(vertices, normals, offset)

        self._colors = np.asarray(colors)

        N = len(self._vertices)
        if len(self._colors.shape) == 1:
            if self._colors.size == 3:
                self._colors = np.array(self._colors.tolist() + [1])
            self._colors = self._colors[np.newaxis].repeat(N, axis=0)
        elif self._colors.shape[1] == 3:
            self._colors = np.hstack([self._colors, np.ones((N, 1))])

    def init(self, ctx):
        self._prog = ctx.program(
            vertex_shader="""
                #version 330

                uniform mat4 mvp;
                uniform mat4 local_model;
                uniform vec3 offset;
                in vec3 in_vert;
                in vec3 in_norm;
                in vec4 in_color;
                out vec3 v_vert;
                out vec3 v_norm;
                out vec4 v_color;

                void main() {
                    v_color = in_color;

                    // Compute the position of the vertex
                    vec4 t_position = vec4(in_vert, 1.0);
                    t_position = local_model * t_position;
                    t_position = t_position + vec4(offset, 0.);
                    v_vert = t_position.xyz / t_position.w;
                    // TODO: We should probably use the global model matrix as
                    //       well but it is left for the future 
                    t_position = mvp * t_position;
                    gl_Position = t_position;

                    // Compute the normal of the vertex
                    vec4 t_normal = vec4(in_norm, 1.0);
                    t_normal = local_model * t_normal;
                    v_norm = t_normal.xyz / t_normal.w;
                }
            """,
            fragment_shader="""
                #version 330

                uniform vec3 light;
                in vec3 v_vert;
                in vec3 v_norm;
                in vec4 v_color;

                out vec4 f_color;

                void main() {
                    float lum = dot(normalize(v_norm), normalize(v_vert - light));
                    lum = acos(lum) / 3.14159265;
                    lum = clamp(lum, 0.0, 1.0);

                    f_color = vec4(v_color.xyz * lum, v_color.w);
                }
            """
        )
        self._vbo = ctx.buffer(np.hstack([
            self._vertices,
            self._normals,
            self._colors
        ]).astype(np.float32).tobytes())
        self._vao = ctx.simple_vertex_array(
            self._prog,
            self._vbo,
            "in_vert", "in_norm", "in_color"
        )
        self.model_matrix = self._model_matrix
        self.offset = self._offset

    def _get_uniforms_list(self):
        """Return the used uniforms to fetch from the scene."""
        return ["light", "mvp"]

    def _update_vbo(self):
        """Write in the vertex buffer object the vertices, normals and
        colors."""
        if self._vbo is not None:
            self._vbo.write(np.hstack([
                self._vertices, self._normals, self._colors
            ]).astype(np.float32).tobytes())

    def sort_triangles(self, point):
        """Sort the triangles such that the first is furthest from `point` and
        the last is the closest to `point`.

        It is used so that transparency works properly in OpenGL.
        """
        vertices = self._vertices.reshape(-1, 3, 3)
        normals = self._normals.reshape(-1, 9)
        colors = self._colors.reshape(-1, 12)

        centers = vertices.mean(-2)
        d = ((np.asarray(point).reshape(1, 3) - centers)**2).sum(-1)
        alpha = (colors[:, ::4].mean(-1)<1).astype(np.float32) * 1000
        idxs = np.argsort(d+alpha)[::-1]

        self._vertices = vertices[idxs].reshape(-1, 3)
        self._normals = normals[idxs].reshape(-1, 3)
        self._colors = colors[idxs].reshape(-1, 4)
        self._update_vbo()

    def scale(self, s):
        """Multiply all the vertices with a number s."""
        self._vertices *= s
        self._update_vbo()

    def translate(self, t):
        """Translate all the vertices with a vector t."""
        self._vertices += t
        self._update_vbo()

    def to_unit_cube(self):
        """Transform the mesh such that it fits in the 0 centered unit cube."""
        bbox = self.bbox
        dims = bbox[1] - bbox[0]
        self._vertices -= dims/2 + bbox[0]
        self._vertices /= dims.max()
        self._update_vbo()

    @classmethod
    def from_file(cls, filepath, color=(0.3, 0.3, 0.3), ext=None):
        """Read the mesh from a file.

        Arguments
        ---------
            filepath: Path to file or file object containing the mesh
            color: A default color to load if the information is not provided
                   in the file
            ext: The file extension (including the dot) if `filepath` is an
                 object
        """
        # Read the mesh
        mesh = read_mesh_file(filepath, ext=ext)

        # Extract the triangles
        vertices = mesh.vertices

        # Set a normal per triangle vertex
        try:
            normals = mesh.normals
        except NotImplementedError:
            normals = np.repeat(Mesh._triangle_normals(vertices), 3, axis=0)

        # Set a color per triangle vertex
        try:
            colors = mesh.colors
        except NotImplementedError:
            colors = np.ones((len(vertices), 1)) * color

        return cls(vertices, normals, colors)

    @classmethod
    def from_xyz(cls, X, Y, Z, colormap=None):
        X, Y, Z = list(map(np.asarray, [X, Y, Z]))
        def gray(x):
            return np.ones((x.shape[0], 3))*x[:, np.newaxis]

        def normalize(x):
            xmin = x.min()
            xmax = x.max()
            return 2*(x-xmin)/(xmax-xmin) - 1

        def idx(i, j, x):
            return i*x.shape[1] + j

        # Normalize dimensions in [-1, 1]
        x = normalize(X)
        y = normalize(Y)
        z = normalize(Z)

        # Create faces by triangulating each quad
        faces = []
        for i in range(x.shape[0]-1):
            for j in range(y.shape[1]-1):
                # i, j; i, j+1; i+1; j+1
                # i, j; i+1, j; i+1; j+1
                faces.extend([
                    idx(i+1, j+1, x),
                    idx(i, j+1, x),
                    idx(i, j, x),
                    idx(i+1, j, x),
                    idx(i+1, j+1, x),
                    idx(i, j, x)
                ])

        vertices = np.vstack([x.ravel(), y.ravel(), z.ravel()]).T[faces]
        colors = (
            colormap(Z.ravel()[faces])
            if colormap else gray(z.ravel()[faces])
        )
        normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)

        return cls(vertices, normals, colors)

    @classmethod
    def from_faces(cls, vertices, faces, colors):
        vertices, faces, colors = list(map(
            np.asarray,
            [vertices, faces, colors]
        ))
        vertices = vertices[faces].reshape(-1, 3)
        normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)
        if len(colors.shape) != 1:
            colors = colors[faces].reshape(-1, 3)

        return cls(vertices, normals, colors)

    @classmethod
    def from_boxes(cls, centers, sizes, colors):
        """Create boxes.

        Arguments
        ---------
            centers: Array of 3 dimensional centers
            sizes: Array of 3 sizes per box that give, half the width, half the
                   depth, half the height
            colors: tuple for all boxes or array of colors per box
        """
        box = np.array([[-1, -1,  1],
                        [ 1, -1,  1],
                        [ 1,  1,  1],
                        [-1, -1,  1],
                        [ 1,  1,  1],
                        [-1,  1,  1],
                        [-1,  1, -1],
                        [ 1,  1,  1],
                        [-1,  1,  1],
                        [-1,  1, -1],
                        [ 1,  1, -1],
                        [ 1,  1,  1],
                        [-1,  1, -1],
                        [-1, -1,  1],
                        [-1,  1,  1],
                        [-1,  1, -1],
                        [-1, -1,  1],
                        [-1, -1, -1],
                        [ 1, -1, -1],
                        [ 1, -1,  1],
                        [ 1,  1,  1],
                        [ 1, -1, -1],
                        [ 1,  1, -1],
                        [ 1,  1,  1],
                        [ 1, -1, -1],
                        [-1, -1,  1],
                        [ 1, -1,  1],
                        [ 1, -1, -1],
                        [-1, -1,  1],
                        [-1, -1, -1],
                        [ 1, -1, -1],
                        [-1,  1, -1],
                        [ 1,  1, -1],
                        [ 1, -1, -1],
                        [-1,  1, -1],
                        [-1, -1, -1]]).astype(np.float32)

        normals = np.array([[ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  0,  1],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [ 0,  1,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [-1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 1,  0,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0, -1,  0],
                            [ 0,  0, -1],
                            [ 0,  0, -1],
                            [ 0,  0, -1],
                            [ 0,  0, -1],
                            [ 0,  0, -1],
                            [ 0,  0, -1]]).astype(np.float32)

        centers, sizes, colors = list(map(
            np.asarray,
            [centers, sizes, colors]
        ))

        assert len(centers.shape) == 2 and centers.shape[1] == 3
        if len(sizes.shape) == 1:
            sizes = sizes[np.newaxis].repeat(len(centers), axis=0)
        vertices = centers[:, np.newaxis]+sizes[:, np.newaxis]*box[np.newaxis]
        vertices = vertices.reshape(-1, 3)
        normals = np.vstack([normals]*len(centers))

        if len(colors.shape) == 1:
            if colors.size < 4:
                colors = np.array(colors.tolist() + [1.]*(4-colors.size))
            colors = colors[np.newaxis].repeat(len(vertices), axis=0)
        if len(colors) != len(vertices) and len(colors) == len(centers):
            colors = np.repeat(colors, len(box), axis=0)

        return cls(vertices, normals, colors)

    @classmethod
    def from_voxel_grid(cls, voxels, sizes=None, colors=(0.3, 0.3, 0.3),
                        bbox=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]]):
        """ Create a voxel grid

        Arguments
        ---------
            voxels: Array of 3D values, with truthy values indicating which
                    voxels to fill
            colors: The colors of the voxels. If colors is a vector then
                    it is the same for all voxels. If it is a 4 dimensional
                    tensor then a color per voxel is assumed.
        """
        # Make sure voxels, colors and bbox are arrays
        voxels, colors, bbox = list(map(np.asarray, [voxels, colors, bbox]))

        # Ensure that the voxel grid is indeed a 3D grid
        assert len(voxels.shape) == 3
        M, N, K = voxels.shape

        # Clean and standardize the sizes
        if sizes is None:
            sizes = (bbox[1]-bbox[0]) * 0.48 / [M, N, K]
        else:
            sizes = np.asarray(sizes)

        # Convert the indices to center coordinates
        x, y, z = np.indices((M, N, K)).astype(np.float32)
        x = x / M * (bbox[1][0] - bbox[0][0]) + bbox[0][0]
        y = y / N * (bbox[1][1] - bbox[0][1]) + bbox[0][1]
        z = z / K * (bbox[1][2] - bbox[0][2]) + bbox[0][2]
        centers = np.vstack([x[voxels], y[voxels], z[voxels]]).T

        # Convert the sizes to per box sizes
        if len(sizes.shape) == 1:
            sizes = np.array([sizes for _ in range(len(centers))])
        elif len(sizes.shape) == 4:
            sizes = sizes[voxels]

        # Convert the colors to per box colors
        if len(colors.shape) == 1:
            colors = np.array([colors for _ in range(len(centers))])
        elif len(colors.shape) == 4:
            colors = colors[voxels]

        return cls.from_boxes(centers=centers, sizes=sizes, colors=colors)

    @classmethod
    def from_binvox(cls, binvoxfile, colors=(0.3, 0.3, 0.3)):
        """Create a voxel grid from a binvox file.

        For the format see https://patrickmin.com/binvox/binvox.html .
        
        Arguments
        ---------
            binvoxfile: str or file object that contains the voxelgrid data in
                        binvox format
            colors: The colors of the voxels to pass to from_voxel_grid().
        """
        voxelgrid, translation, scale = read_binvox(binvoxfile)
        bbox = np.array([[0., 0, 0], [1, 1, 1]]) * scale + translation

        return cls.from_voxel_grid(voxelgrid, colors=colors, bbox=bbox)

    @classmethod
    def from_superquadrics(cls, alpha, epsilon, translation, rotation, colors,
                           offset=[0, 0, 0.], vertex_count=10000):
        """Create Superquadrics.

        Arguments
        ---------
            alpha: Array of 3 sizes, along each axis
            epsilon: Array of 2 shapes, along each a
            translation: Array of 3 dimensional center
            rotation: Array of size 3x3 containing the rotations
            colors: Tuple for all sqs or array of colors per sq
        """
        def fexp(x, p):
            return np.sign(x)*(np.abs(x)**p)

        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

        # triangulate the sphere to be used with the SQs
        n = int(np.sqrt(vertex_count))
        eta = np.linspace(-np.pi/2, np.pi/2, n, endpoint=True)
        omega = np.linspace(-np.pi, np.pi, n, endpoint=True)
        triangles = []
        for o1, o2 in zip(np.roll(omega, 1), omega):
            triangles.extend([
                (eta[0], 0),
                (eta[1], o2),
                (eta[1], o1),
            ])
        for e in range(1, len(eta)-2):
            for o1, o2 in zip(np.roll(omega, 1), omega):
                triangles.extend([
                    (eta[e], o1),
                    (eta[e+1], o2),
                    (eta[e+1], o1),
                    (eta[e], o1),
                    (eta[e], o2),
                    (eta[e+1], o2),
                ])
        for o1, o2 in zip(np.roll(omega, 1), omega):
            triangles.extend([
                (eta[-1], 0),
                (eta[-2], o1),
                (eta[-2], o2),
            ])
        triangles = np.array(triangles)
        eta, omega = triangles[:, 0], triangles[:, 1]

        # collect the pretriangulated vertices of each SQ
        vertices = []
        a, e, t, R = list(map(
            np.asarray,
            [alpha, epsilon, translation, rotation]
        ))
        M, _ = a.shape  # number of superquadrics
        assert R.shape == (M, 3, 3)
        assert t.shape == (M, 3)
        for i in range(M):
            a1, a2, a3 = a[i]
            e1, e2 = e[i]
            x, y, z = sq_surface(a1, a2, a3, e1, e2, eta, omega)
            # Get points on the surface of each SQ
            V = np.stack([x, y, z], axis=-1)
            V = R[i].T.dot(V.T).T + t[i].reshape(1, 3)
            vertices.append(V)

        # Finalize the mesh
        vertices = np.vstack(vertices)
        normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)
        colors = np.asarray(colors)

        if len(colors.shape) == 1:
            if colors.size < 4:
                colors = np.array(colors.tolist() + [1.]*(4-colors.size))
            colors = colors[np.newaxis].repeat(len(vertices), axis=0)
        assert len(colors) == len(vertices) or len(colors) == M
        if len(colors) == M:
            colors = np.repeat(colors, len(vertices) // M, axis=0)

        return cls(vertices, normals, colors, offset)

Ancestors

Static methods

def from_binvox(binvoxfile, colors=(0.3, 0.3, 0.3))

Create a voxel grid from a binvox file.

For the format see https://patrickmin.com/binvox/binvox.html .

Arguments

binvoxfile: str or file object that contains the voxelgrid data in
            binvox format
colors: The colors of the voxels to pass to from_voxel_grid().
Expand source code
@classmethod
def from_binvox(cls, binvoxfile, colors=(0.3, 0.3, 0.3)):
    """Create a voxel grid from a binvox file.

    For the format see https://patrickmin.com/binvox/binvox.html .
    
    Arguments
    ---------
        binvoxfile: str or file object that contains the voxelgrid data in
                    binvox format
        colors: The colors of the voxels to pass to from_voxel_grid().
    """
    voxelgrid, translation, scale = read_binvox(binvoxfile)
    bbox = np.array([[0., 0, 0], [1, 1, 1]]) * scale + translation

    return cls.from_voxel_grid(voxelgrid, colors=colors, bbox=bbox)
def from_boxes(centers, sizes, colors)

Create boxes.

Arguments

centers: Array of 3 dimensional centers
sizes: Array of 3 sizes per box that give, half the width, half the
       depth, half the height
colors: tuple for all boxes or array of colors per box
Expand source code
@classmethod
def from_boxes(cls, centers, sizes, colors):
    """Create boxes.

    Arguments
    ---------
        centers: Array of 3 dimensional centers
        sizes: Array of 3 sizes per box that give, half the width, half the
               depth, half the height
        colors: tuple for all boxes or array of colors per box
    """
    box = np.array([[-1, -1,  1],
                    [ 1, -1,  1],
                    [ 1,  1,  1],
                    [-1, -1,  1],
                    [ 1,  1,  1],
                    [-1,  1,  1],
                    [-1,  1, -1],
                    [ 1,  1,  1],
                    [-1,  1,  1],
                    [-1,  1, -1],
                    [ 1,  1, -1],
                    [ 1,  1,  1],
                    [-1,  1, -1],
                    [-1, -1,  1],
                    [-1,  1,  1],
                    [-1,  1, -1],
                    [-1, -1,  1],
                    [-1, -1, -1],
                    [ 1, -1, -1],
                    [ 1, -1,  1],
                    [ 1,  1,  1],
                    [ 1, -1, -1],
                    [ 1,  1, -1],
                    [ 1,  1,  1],
                    [ 1, -1, -1],
                    [-1, -1,  1],
                    [ 1, -1,  1],
                    [ 1, -1, -1],
                    [-1, -1,  1],
                    [-1, -1, -1],
                    [ 1, -1, -1],
                    [-1,  1, -1],
                    [ 1,  1, -1],
                    [ 1, -1, -1],
                    [-1,  1, -1],
                    [-1, -1, -1]]).astype(np.float32)

    normals = np.array([[ 0,  0,  1],
                        [ 0,  0,  1],
                        [ 0,  0,  1],
                        [ 0,  0,  1],
                        [ 0,  0,  1],
                        [ 0,  0,  1],
                        [ 0,  1,  0],
                        [ 0,  1,  0],
                        [ 0,  1,  0],
                        [ 0,  1,  0],
                        [ 0,  1,  0],
                        [ 0,  1,  0],
                        [-1,  0,  0],
                        [-1,  0,  0],
                        [-1,  0,  0],
                        [-1,  0,  0],
                        [-1,  0,  0],
                        [-1,  0,  0],
                        [ 1,  0,  0],
                        [ 1,  0,  0],
                        [ 1,  0,  0],
                        [ 1,  0,  0],
                        [ 1,  0,  0],
                        [ 1,  0,  0],
                        [ 0, -1,  0],
                        [ 0, -1,  0],
                        [ 0, -1,  0],
                        [ 0, -1,  0],
                        [ 0, -1,  0],
                        [ 0, -1,  0],
                        [ 0,  0, -1],
                        [ 0,  0, -1],
                        [ 0,  0, -1],
                        [ 0,  0, -1],
                        [ 0,  0, -1],
                        [ 0,  0, -1]]).astype(np.float32)

    centers, sizes, colors = list(map(
        np.asarray,
        [centers, sizes, colors]
    ))

    assert len(centers.shape) == 2 and centers.shape[1] == 3
    if len(sizes.shape) == 1:
        sizes = sizes[np.newaxis].repeat(len(centers), axis=0)
    vertices = centers[:, np.newaxis]+sizes[:, np.newaxis]*box[np.newaxis]
    vertices = vertices.reshape(-1, 3)
    normals = np.vstack([normals]*len(centers))

    if len(colors.shape) == 1:
        if colors.size < 4:
            colors = np.array(colors.tolist() + [1.]*(4-colors.size))
        colors = colors[np.newaxis].repeat(len(vertices), axis=0)
    if len(colors) != len(vertices) and len(colors) == len(centers):
        colors = np.repeat(colors, len(box), axis=0)

    return cls(vertices, normals, colors)
def from_faces(vertices, faces, colors)
Expand source code
@classmethod
def from_faces(cls, vertices, faces, colors):
    vertices, faces, colors = list(map(
        np.asarray,
        [vertices, faces, colors]
    ))
    vertices = vertices[faces].reshape(-1, 3)
    normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)
    if len(colors.shape) != 1:
        colors = colors[faces].reshape(-1, 3)

    return cls(vertices, normals, colors)
def from_file(filepath, color=(0.3, 0.3, 0.3), ext=None)

Read the mesh from a file.

Arguments

filepath: Path to file or file object containing the mesh
color: A default color to load if the information is not provided
       in the file
ext: The file extension (including the dot) if `filepath` is an
     object
Expand source code
@classmethod
def from_file(cls, filepath, color=(0.3, 0.3, 0.3), ext=None):
    """Read the mesh from a file.

    Arguments
    ---------
        filepath: Path to file or file object containing the mesh
        color: A default color to load if the information is not provided
               in the file
        ext: The file extension (including the dot) if `filepath` is an
             object
    """
    # Read the mesh
    mesh = read_mesh_file(filepath, ext=ext)

    # Extract the triangles
    vertices = mesh.vertices

    # Set a normal per triangle vertex
    try:
        normals = mesh.normals
    except NotImplementedError:
        normals = np.repeat(Mesh._triangle_normals(vertices), 3, axis=0)

    # Set a color per triangle vertex
    try:
        colors = mesh.colors
    except NotImplementedError:
        colors = np.ones((len(vertices), 1)) * color

    return cls(vertices, normals, colors)
def from_superquadrics(alpha, epsilon, translation, rotation, colors, offset=[0, 0, 0.0], vertex_count=10000)

Create Superquadrics.

Arguments

alpha: Array of 3 sizes, along each axis
epsilon: Array of 2 shapes, along each a
translation: Array of 3 dimensional center
rotation: Array of size 3x3 containing the rotations
colors: Tuple for all sqs or array of colors per sq
Expand source code
@classmethod
def from_superquadrics(cls, alpha, epsilon, translation, rotation, colors,
                       offset=[0, 0, 0.], vertex_count=10000):
    """Create Superquadrics.

    Arguments
    ---------
        alpha: Array of 3 sizes, along each axis
        epsilon: Array of 2 shapes, along each a
        translation: Array of 3 dimensional center
        rotation: Array of size 3x3 containing the rotations
        colors: Tuple for all sqs or array of colors per sq
    """
    def fexp(x, p):
        return np.sign(x)*(np.abs(x)**p)

    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

    # triangulate the sphere to be used with the SQs
    n = int(np.sqrt(vertex_count))
    eta = np.linspace(-np.pi/2, np.pi/2, n, endpoint=True)
    omega = np.linspace(-np.pi, np.pi, n, endpoint=True)
    triangles = []
    for o1, o2 in zip(np.roll(omega, 1), omega):
        triangles.extend([
            (eta[0], 0),
            (eta[1], o2),
            (eta[1], o1),
        ])
    for e in range(1, len(eta)-2):
        for o1, o2 in zip(np.roll(omega, 1), omega):
            triangles.extend([
                (eta[e], o1),
                (eta[e+1], o2),
                (eta[e+1], o1),
                (eta[e], o1),
                (eta[e], o2),
                (eta[e+1], o2),
            ])
    for o1, o2 in zip(np.roll(omega, 1), omega):
        triangles.extend([
            (eta[-1], 0),
            (eta[-2], o1),
            (eta[-2], o2),
        ])
    triangles = np.array(triangles)
    eta, omega = triangles[:, 0], triangles[:, 1]

    # collect the pretriangulated vertices of each SQ
    vertices = []
    a, e, t, R = list(map(
        np.asarray,
        [alpha, epsilon, translation, rotation]
    ))
    M, _ = a.shape  # number of superquadrics
    assert R.shape == (M, 3, 3)
    assert t.shape == (M, 3)
    for i in range(M):
        a1, a2, a3 = a[i]
        e1, e2 = e[i]
        x, y, z = sq_surface(a1, a2, a3, e1, e2, eta, omega)
        # Get points on the surface of each SQ
        V = np.stack([x, y, z], axis=-1)
        V = R[i].T.dot(V.T).T + t[i].reshape(1, 3)
        vertices.append(V)

    # Finalize the mesh
    vertices = np.vstack(vertices)
    normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)
    colors = np.asarray(colors)

    if len(colors.shape) == 1:
        if colors.size < 4:
            colors = np.array(colors.tolist() + [1.]*(4-colors.size))
        colors = colors[np.newaxis].repeat(len(vertices), axis=0)
    assert len(colors) == len(vertices) or len(colors) == M
    if len(colors) == M:
        colors = np.repeat(colors, len(vertices) // M, axis=0)

    return cls(vertices, normals, colors, offset)
def from_voxel_grid(voxels, sizes=None, colors=(0.3, 0.3, 0.3), bbox=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]])

Create a voxel grid

Arguments

voxels: Array of 3D values, with truthy values indicating which
        voxels to fill
colors: The colors of the voxels. If colors is a vector then
        it is the same for all voxels. If it is a 4 dimensional
        tensor then a color per voxel is assumed.
Expand source code
@classmethod
def from_voxel_grid(cls, voxels, sizes=None, colors=(0.3, 0.3, 0.3),
                    bbox=[[-0.5, -0.5, -0.5], [0.5, 0.5, 0.5]]):
    """ Create a voxel grid

    Arguments
    ---------
        voxels: Array of 3D values, with truthy values indicating which
                voxels to fill
        colors: The colors of the voxels. If colors is a vector then
                it is the same for all voxels. If it is a 4 dimensional
                tensor then a color per voxel is assumed.
    """
    # Make sure voxels, colors and bbox are arrays
    voxels, colors, bbox = list(map(np.asarray, [voxels, colors, bbox]))

    # Ensure that the voxel grid is indeed a 3D grid
    assert len(voxels.shape) == 3
    M, N, K = voxels.shape

    # Clean and standardize the sizes
    if sizes is None:
        sizes = (bbox[1]-bbox[0]) * 0.48 / [M, N, K]
    else:
        sizes = np.asarray(sizes)

    # Convert the indices to center coordinates
    x, y, z = np.indices((M, N, K)).astype(np.float32)
    x = x / M * (bbox[1][0] - bbox[0][0]) + bbox[0][0]
    y = y / N * (bbox[1][1] - bbox[0][1]) + bbox[0][1]
    z = z / K * (bbox[1][2] - bbox[0][2]) + bbox[0][2]
    centers = np.vstack([x[voxels], y[voxels], z[voxels]]).T

    # Convert the sizes to per box sizes
    if len(sizes.shape) == 1:
        sizes = np.array([sizes for _ in range(len(centers))])
    elif len(sizes.shape) == 4:
        sizes = sizes[voxels]

    # Convert the colors to per box colors
    if len(colors.shape) == 1:
        colors = np.array([colors for _ in range(len(centers))])
    elif len(colors.shape) == 4:
        colors = colors[voxels]

    return cls.from_boxes(centers=centers, sizes=sizes, colors=colors)
def from_xyz(X, Y, Z, colormap=None)
Expand source code
@classmethod
def from_xyz(cls, X, Y, Z, colormap=None):
    X, Y, Z = list(map(np.asarray, [X, Y, Z]))
    def gray(x):
        return np.ones((x.shape[0], 3))*x[:, np.newaxis]

    def normalize(x):
        xmin = x.min()
        xmax = x.max()
        return 2*(x-xmin)/(xmax-xmin) - 1

    def idx(i, j, x):
        return i*x.shape[1] + j

    # Normalize dimensions in [-1, 1]
    x = normalize(X)
    y = normalize(Y)
    z = normalize(Z)

    # Create faces by triangulating each quad
    faces = []
    for i in range(x.shape[0]-1):
        for j in range(y.shape[1]-1):
            # i, j; i, j+1; i+1; j+1
            # i, j; i+1, j; i+1; j+1
            faces.extend([
                idx(i+1, j+1, x),
                idx(i, j+1, x),
                idx(i, j, x),
                idx(i+1, j, x),
                idx(i+1, j+1, x),
                idx(i, j, x)
            ])

    vertices = np.vstack([x.ravel(), y.ravel(), z.ravel()]).T[faces]
    colors = (
        colormap(Z.ravel()[faces])
        if colormap else gray(z.ravel()[faces])
    )
    normals = np.repeat(cls._triangle_normals(vertices), 3, axis=0)

    return cls(vertices, normals, colors)

Methods

def sort_triangles(self, point)

Sort the triangles such that the first is furthest from point and the last is the closest to point.

It is used so that transparency works properly in OpenGL.

Expand source code
def sort_triangles(self, point):
    """Sort the triangles such that the first is furthest from `point` and
    the last is the closest to `point`.

    It is used so that transparency works properly in OpenGL.
    """
    vertices = self._vertices.reshape(-1, 3, 3)
    normals = self._normals.reshape(-1, 9)
    colors = self._colors.reshape(-1, 12)

    centers = vertices.mean(-2)
    d = ((np.asarray(point).reshape(1, 3) - centers)**2).sum(-1)
    alpha = (colors[:, ::4].mean(-1)<1).astype(np.float32) * 1000
    idxs = np.argsort(d+alpha)[::-1]

    self._vertices = vertices[idxs].reshape(-1, 3)
    self._normals = normals[idxs].reshape(-1, 3)
    self._colors = colors[idxs].reshape(-1, 4)
    self._update_vbo()
def translate(self, t)

Translate all the vertices with a vector t.

Expand source code
def translate(self, t):
    """Translate all the vertices with a vector t."""
    self._vertices += t
    self._update_vbo()

Inherited members

class MeshBase (vertices, normals, offset=[0, 0, 0.0])

Abstract base class that implements functions commonly used by the subclasses.

Expand source code
class MeshBase(Renderable):
    """Abstract base class that implements functions commonly used by the
    subclasses."""
    def __init__(self, vertices, normals, offset=[0, 0, 0.]):
        self._vertices = np.asarray(vertices)
        self._normals = np.asarray(normals)
        self._model_matrix = np.eye(4).astype(np.float32)
        self._offset = np.asarray(offset).astype(np.float32)

        self._prog = None
        self._vbo = None
        self._vao = None

    @property
    def bbox(self):
        """The axis aligned bounding box of all the vertices as two
        3-dimensional arrays containing the minimum and maximum for each
        axis."""
        return [
            self._vertices.min(axis=0),
            self._vertices.max(axis=0)
        ]

    @property
    def model_matrix(self):
        """An affine transformation matrix (4x4) applied to the mesh before
        rendering. Can be changed to animate the mesh."""
        return self._model_matrix

    @model_matrix.setter
    def model_matrix(self, v):
        self._model_matrix = np.asarray(v).astype(np.float32)
        if self._prog:
            self._prog["local_model"].write(self._model_matrix.tobytes())

    def rotate_x(self, angle):
        """Helper function that multiplies the `model_matrix` with a rotation
        matrix around the x axis."""
        m = Matrix44.from_x_rotation(angle)
        self.model_matrix = m.dot(self.model_matrix)

    def rotate_y(self, angle):
        """Helper function that multiplies the `model_matrix` with a rotation
        matrix around the y axis."""
        m = Matrix44.from_y_rotation(angle)
        self.model_matrix = m.dot(self.model_matrix)

    def rotate_z(self, angle):
        """Helper function that multiplies the `model_matrix` with a rotation
        matrix around the z axis."""
        m = Matrix44.from_z_rotation(angle)
        self.model_matrix = m.dot(self.model_matrix)

    def rotate_axis(self, axis, angle):
        """Helper function that multiplies the `model_matrix` with a rotation
        matrix around the passed in axis."""
        m = matrix44.create_from_axis_rotation(axis, angle)
        self.model_matrix = m.dot(self.model_matrix)

    @property
    def offset(self):
        """A translation vector for the mesh vertices."""
        return self._offset

    @offset.setter
    def offset(self, v):
        self._offset = np.asarray(v).astype(np.float32)
        if self._prog:
            self._prog["offset"].write(self._offset.tobytes())

    def scale(self, s):
        """Multiply all the vertices with a number s."""
        self._vertices *= s
        self._update_vbo()

    def affine_transform(self, R=np.eye(3), t=np.zeros(3)):
        """Rotate and translate the vertices and then update the gpu buffer.

        Given the vertices v \in R^{Nx3} this function implements

            v' = v @ R + t

        Arguments
        ---------
            R: array (3, 3), the 3x3 rotation matrix
            t: array (3,), the translation vector
        """
        self._vertices = self._vertices.dot(R) + t
        self._update_vbo()

    def to_unit_cube(self):
        """Transform the mesh such that it fits in the 0 centered unit cube."""
        bbox = self.bbox
        dims = bbox[1] - bbox[0]
        self._vertices -= dims/2 + bbox[0]
        self._vertices /= dims.max()
        self._update_vbo()

    def release(self):
        self._prog.release()
        self._vbo.release()
        self._vao.release()

    def render(self):
        self._vao.render()

    def update_uniforms(self, uniforms):
        uniforms_list = self._get_uniforms_list()
        for k, v in uniforms:
            if k in uniforms_list:
                self._prog[k].write(v.tobytes())

    @staticmethod
    def _triangle_normals(triangles):
        triangles = triangles.reshape(-1, 3, 3)
        ba = triangles[:, 1] - triangles[:, 0]
        bc = triangles[:, 2] - triangles[:, 1]
        return np.cross(ba, bc, axis=-1)

    def _update_vbo(self):
        """Update the vertex buffer object because one of the values has
        changed (vertices, normals, etc)."""
        raise NotImplementedError()

Ancestors

Subclasses

Instance variables

var bbox

The axis aligned bounding box of all the vertices as two 3-dimensional arrays containing the minimum and maximum for each axis.

Expand source code
@property
def bbox(self):
    """The axis aligned bounding box of all the vertices as two
    3-dimensional arrays containing the minimum and maximum for each
    axis."""
    return [
        self._vertices.min(axis=0),
        self._vertices.max(axis=0)
    ]
var model_matrix

An affine transformation matrix (4x4) applied to the mesh before rendering. Can be changed to animate the mesh.

Expand source code
@property
def model_matrix(self):
    """An affine transformation matrix (4x4) applied to the mesh before
    rendering. Can be changed to animate the mesh."""
    return self._model_matrix
var offset

A translation vector for the mesh vertices.

Expand source code
@property
def offset(self):
    """A translation vector for the mesh vertices."""
    return self._offset

Methods

def affine_transform(self, R=array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]), t=array([0., 0., 0.]))

Rotate and translate the vertices and then update the gpu buffer.

Given the vertices v \in R^{Nx3} this function implements

v' = v @ R + t

Arguments

R: array (3, 3), the 3x3 rotation matrix
t: array (3,), the translation vector
Expand source code
def affine_transform(self, R=np.eye(3), t=np.zeros(3)):
    """Rotate and translate the vertices and then update the gpu buffer.

    Given the vertices v \in R^{Nx3} this function implements

        v' = v @ R + t

    Arguments
    ---------
        R: array (3, 3), the 3x3 rotation matrix
        t: array (3,), the translation vector
    """
    self._vertices = self._vertices.dot(R) + t
    self._update_vbo()
def rotate_axis(self, axis, angle)

Helper function that multiplies the model_matrix with a rotation matrix around the passed in axis.

Expand source code
def rotate_axis(self, axis, angle):
    """Helper function that multiplies the `model_matrix` with a rotation
    matrix around the passed in axis."""
    m = matrix44.create_from_axis_rotation(axis, angle)
    self.model_matrix = m.dot(self.model_matrix)
def rotate_x(self, angle)

Helper function that multiplies the model_matrix with a rotation matrix around the x axis.

Expand source code
def rotate_x(self, angle):
    """Helper function that multiplies the `model_matrix` with a rotation
    matrix around the x axis."""
    m = Matrix44.from_x_rotation(angle)
    self.model_matrix = m.dot(self.model_matrix)
def rotate_y(self, angle)

Helper function that multiplies the model_matrix with a rotation matrix around the y axis.

Expand source code
def rotate_y(self, angle):
    """Helper function that multiplies the `model_matrix` with a rotation
    matrix around the y axis."""
    m = Matrix44.from_y_rotation(angle)
    self.model_matrix = m.dot(self.model_matrix)
def rotate_z(self, angle)

Helper function that multiplies the model_matrix with a rotation matrix around the z axis.

Expand source code
def rotate_z(self, angle):
    """Helper function that multiplies the `model_matrix` with a rotation
    matrix around the z axis."""
    m = Matrix44.from_z_rotation(angle)
    self.model_matrix = m.dot(self.model_matrix)
def scale(self, s)

Multiply all the vertices with a number s.

Expand source code
def scale(self, s):
    """Multiply all the vertices with a number s."""
    self._vertices *= s
    self._update_vbo()
def to_unit_cube(self)

Transform the mesh such that it fits in the 0 centered unit cube.

Expand source code
def to_unit_cube(self):
    """Transform the mesh such that it fits in the 0 centered unit cube."""
    bbox = self.bbox
    dims = bbox[1] - bbox[0]
    self._vertices -= dims/2 + bbox[0]
    self._vertices /= dims.max()
    self._update_vbo()

Inherited members