Source code for colour.geometry.primitives

# -*- coding: utf-8 -*-
"""
Geometry Primitives
===================

Defines various geometry primitives and their generation methods:

-   :func:`colour.geometry.PLANE_TO_AXIS_MAPPING`
-   :func:`colour.geometry.primitive_grid`
-   :func:`colour.geometry.primitive_cube`
-   :func:`colour.PRIMITIVE_METHODS`
-   :func:`colour.primitive`

References
----------
-   :cite:`Cabello2015` : Cabello, R. (n.d.). PlaneGeometry.js. Retrieved May
    12, 2015, from
    https://github.com/mrdoob/three.js/blob/dev/src/geometries/PlaneGeometry.js
"""

import numpy as np

from colour.constants import DEFAULT_INT_DTYPE, DEFAULT_FLOAT_DTYPE
from colour.utilities import (CaseInsensitiveMapping, filter_kwargs, ones,
                              zeros, validate_method)

__author__ = 'Colour Developers'
__copyright__ = 'Copyright (C) 2013-2021 - Colour Developers'
__license__ = 'New BSD License - https://opensource.org/licenses/BSD-3-Clause'
__maintainer__ = 'Colour Developers'
__email__ = 'colour-developers@colour-science.org'
__status__ = 'Production'

__all__ = [
    'PLANE_TO_AXIS_MAPPING',
    'primitive_grid',
    'primitive_cube',
    'PRIMITIVE_METHODS',
    'primitive',
]

PLANE_TO_AXIS_MAPPING = CaseInsensitiveMapping({
    'yz': '+x',
    'zy': '-x',
    'xz': '+y',
    'zx': '-y',
    'xy': '+z',
    'yx': '-z',
})
PLANE_TO_AXIS_MAPPING.__doc__ = """
Plane to axis mapping.

PLANE_TO_AXIS_MAPPING : CaseInsensitiveMapping
    **{'-x', '+x', '-y', '+y', '-z', '+z'}**
"""


[docs]def primitive_grid(width=1, height=1, width_segments=1, height_segments=1, axis='+z'): """ Generates vertices and indices for a filled and outlined grid primitive. Parameters ---------- width : float, optional Grid width. height : float, optional Grid height. width_segments : int, optional Grid segments count along the width. height_segments : float, optional Grid segments count along the height. axis : unicode, optional **{'+z', '-x', '+x', '-y', '+y', '-z', 'xy', 'xz', 'yz', 'yx', 'zx', 'zy'}**, Axis the primitive will be normal to, or plane the primitive will be co-planar with. Returns ------- tuple Tuple of grid vertices, face indices to produce a filled grid and outline indices to produce an outline of the faces of the grid. References ---------- :cite:`Cabello2015` Examples -------- >>> vertices, faces, outline = primitive_grid() >>> print(vertices) [([-0.5, 0.5, 0. ], [ 0., 1.], [ 0., 0., 1.], [ 0., 1., 0., 1.]) ([ 0.5, 0.5, 0. ], [ 1., 1.], [ 0., 0., 1.], [ 1., 1., 0., 1.]) ([-0.5, -0.5, 0. ], [ 0., 0.], [ 0., 0., 1.], [ 0., 0., 0., 1.]) ([ 0.5, -0.5, 0. ], [ 1., 0.], [ 0., 0., 1.], [ 1., 0., 0., 1.])] >>> print(faces) [[0 2 1] [2 3 1]] >>> print(outline) [[0 2] [2 3] [3 1] [1 0]] """ axis = PLANE_TO_AXIS_MAPPING.get(axis, axis).lower() x_grid = width_segments y_grid = height_segments x_grid1 = x_grid + 1 y_grid1 = y_grid + 1 # Positions, normals and uvs. positions = zeros(x_grid1 * y_grid1 * 3) normals = zeros(x_grid1 * y_grid1 * 3) uvs = zeros(x_grid1 * y_grid1 * 2) y = np.arange(y_grid1) * height / y_grid - height / 2 x = np.arange(x_grid1) * width / x_grid - width / 2 positions[::3] = np.tile(x, y_grid1) positions[1::3] = -np.repeat(y, x_grid1) normals[2::3] = 1 uvs[::2] = np.tile(np.arange(x_grid1) / x_grid, y_grid1) uvs[1::2] = np.repeat(1 - np.arange(y_grid1) / y_grid, x_grid1) # Faces and outline. faces, outline = [], [] for i_y in range(y_grid): for i_x in range(x_grid): a = i_x + x_grid1 * i_y b = i_x + x_grid1 * (i_y + 1) c = (i_x + 1) + x_grid1 * (i_y + 1) d = (i_x + 1) + x_grid1 * i_y faces.extend([(a, b, d), (b, c, d)]) outline.extend([(a, b), (b, c), (c, d), (d, a)]) positions = np.reshape(positions, (-1, 3)) uvs = np.reshape(uvs, (-1, 2)) normals = np.reshape(normals, (-1, 3)) faces = np.reshape(faces, (-1, 3)).astype(np.uint32) outline = np.reshape(outline, (-1, 2)).astype(np.uint32) if axis in ('-x', '+x'): shift, zero_axis = 1, 0 elif axis in ('-y', '+y'): shift, zero_axis = -1, 1 elif axis in ('-z', '+z'): shift, zero_axis = 0, 2 sign = -1 if '-' in axis else 1 positions = np.roll(positions, shift, -1) normals = np.roll(normals, shift, -1) * sign vertex_colours = np.ravel(positions) vertex_colours = np.hstack([ np.reshape( np.interp(vertex_colours, (np.min(vertex_colours), np.max(vertex_colours)), (0, 1)), positions.shape), ones([positions.shape[0], 1]) ]) vertex_colours[..., zero_axis] = 0 vertices = zeros(positions.shape[0], [ ('position', DEFAULT_FLOAT_DTYPE, 3), ('uv', DEFAULT_FLOAT_DTYPE, 2), ('normal', DEFAULT_FLOAT_DTYPE, 3), ('colour', DEFAULT_FLOAT_DTYPE, 4), ]) vertices['position'] = positions vertices['uv'] = uvs vertices['normal'] = normals vertices['colour'] = vertex_colours return vertices, faces, outline
[docs]def primitive_cube(width=1, height=1, depth=1, width_segments=1, height_segments=1, depth_segments=1, planes=None): """ Generates vertices and indices for a filled and outlined cube primitive. Parameters ---------- width : float, optional Cube width. height : float, optional Cube height. depth : float, optional Cube depth. width_segments : int, optional Cube segments count along the width. height_segments : float, optional Cube segments count along the height. depth_segments : float, optional Cube segments count along the depth. planes : array_like, optional **{'-x', '+x', '-y', '+y', '-z', '+z', 'xy', 'xz', 'yz', 'yx', 'zx', 'zy'}**, Grid primitives to include in the cube construction. Returns ------- tuple Tuple of cube vertices, face indices to produce a filled cube and outline indices to produce an outline of the faces of the cube. Examples -------- >>> vertices, faces, outline = primitive_cube() >>> print(vertices) [([-0.5, 0.5, -0.5], [ 0., 1.], [-0., -0., -1.], [ 0., 1., 0., 1.]) ([ 0.5, 0.5, -0.5], [ 1., 1.], [-0., -0., -1.], [ 1., 1., 0., 1.]) ([-0.5, -0.5, -0.5], [ 0., 0.], [-0., -0., -1.], [ 0., 0., 0., 1.]) ([ 0.5, -0.5, -0.5], [ 1., 0.], [-0., -0., -1.], [ 1., 0., 0., 1.]) ([-0.5, 0.5, 0.5], [ 0., 1.], [ 0., 0., 1.], [ 0., 1., 1., 1.]) ([ 0.5, 0.5, 0.5], [ 1., 1.], [ 0., 0., 1.], [ 1., 1., 1., 1.]) ([-0.5, -0.5, 0.5], [ 0., 0.], [ 0., 0., 1.], [ 0., 0., 1., 1.]) ([ 0.5, -0.5, 0.5], [ 1., 0.], [ 0., 0., 1.], [ 1., 0., 1., 1.]) ([ 0.5, -0.5, -0.5], [ 0., 1.], [-0., -1., -0.], [ 1., 0., 0., 1.]) ([ 0.5, -0.5, 0.5], [ 1., 1.], [-0., -1., -0.], [ 1., 0., 1., 1.]) ([-0.5, -0.5, -0.5], [ 0., 0.], [-0., -1., -0.], [ 0., 0., 0., 1.]) ([-0.5, -0.5, 0.5], [ 1., 0.], [-0., -1., -0.], [ 0., 0., 1., 1.]) ([ 0.5, 0.5, -0.5], [ 0., 1.], [ 0., 1., 0.], [ 1., 1., 0., 1.]) ([ 0.5, 0.5, 0.5], [ 1., 1.], [ 0., 1., 0.], [ 1., 1., 1., 1.]) ([-0.5, 0.5, -0.5], [ 0., 0.], [ 0., 1., 0.], [ 0., 1., 0., 1.]) ([-0.5, 0.5, 0.5], [ 1., 0.], [ 0., 1., 0.], [ 0., 1., 1., 1.]) ([-0.5, -0.5, 0.5], [ 0., 1.], [-1., -0., -0.], [ 0., 0., 1., 1.]) ([-0.5, 0.5, 0.5], [ 1., 1.], [-1., -0., -0.], [ 0., 1., 1., 1.]) ([-0.5, -0.5, -0.5], [ 0., 0.], [-1., -0., -0.], [ 0., 0., 0., 1.]) ([-0.5, 0.5, -0.5], [ 1., 0.], [-1., -0., -0.], [ 0., 1., 0., 1.]) ([ 0.5, -0.5, 0.5], [ 0., 1.], [ 1., 0., 0.], [ 1., 0., 1., 1.]) ([ 0.5, 0.5, 0.5], [ 1., 1.], [ 1., 0., 0.], [ 1., 1., 1., 1.]) ([ 0.5, -0.5, -0.5], [ 0., 0.], [ 1., 0., 0.], [ 1., 0., 0., 1.]) ([ 0.5, 0.5, -0.5], [ 1., 0.], [ 1., 0., 0.], [ 1., 1., 0., 1.])] >>> print(faces) [[ 1 2 0] [ 1 3 2] [ 4 6 5] [ 6 7 5] [ 9 10 8] [ 9 11 10] [12 14 13] [14 15 13] [17 18 16] [17 19 18] [20 22 21] [22 23 21]] >>> print(outline) [[ 0 2] [ 2 3] [ 3 1] [ 1 0] [ 4 6] [ 6 7] [ 7 5] [ 5 4] [ 8 10] [10 11] [11 9] [ 9 8] [12 14] [14 15] [15 13] [13 12] [16 18] [18 19] [19 17] [17 16] [20 22] [22 23] [23 21] [21 20]] """ planes = (sorted(list( PLANE_TO_AXIS_MAPPING.values())) if planes is None else [ PLANE_TO_AXIS_MAPPING.get(plane, plane).lower() for plane in planes ]) w_s, h_s, d_s = width_segments, height_segments, depth_segments planes_m = [] if '-z' in planes: planes_m.append(list(primitive_grid(width, depth, w_s, d_s, '-z'))) planes_m[-1][0]['position'][..., 2] -= height / 2 planes_m[-1][1] = np.fliplr(planes_m[-1][1]) if '+z' in planes: planes_m.append(list(primitive_grid(width, depth, w_s, d_s, '+z'))) planes_m[-1][0]['position'][..., 2] += height / 2 if '-y' in planes: planes_m.append(list(primitive_grid(height, width, h_s, w_s, '-y'))) planes_m[-1][0]['position'][..., 1] -= depth / 2 planes_m[-1][1] = np.fliplr(planes_m[-1][1]) if '+y' in planes: planes_m.append(list(primitive_grid(height, width, h_s, w_s, '+y'))) planes_m[-1][0]['position'][..., 1] += depth / 2 if '-x' in planes: planes_m.append(list(primitive_grid(depth, height, d_s, h_s, '-x'))) planes_m[-1][0]['position'][..., 0] -= width / 2 planes_m[-1][1] = np.fliplr(planes_m[-1][1]) if '+x' in planes: planes_m.append(list(primitive_grid(depth, height, d_s, h_s, '+x'))) planes_m[-1][0]['position'][..., 0] += width / 2 positions = zeros([0, 3]) uvs = zeros([0, 2]) normals = zeros([0, 3]) faces = zeros([0, 3], dtype=DEFAULT_INT_DTYPE) outline = zeros([0, 2], dtype=DEFAULT_INT_DTYPE) offset = 0 for vertices_p, faces_p, outline_p in planes_m: positions = np.vstack([positions, vertices_p['position']]) uvs = np.vstack([uvs, vertices_p['uv']]) normals = np.vstack([normals, vertices_p['normal']]) faces = np.vstack([faces, faces_p + offset]) outline = np.vstack([outline, outline_p + offset]) offset += vertices_p['position'].shape[0] vertices = zeros(positions.shape[0], [('position', DEFAULT_FLOAT_DTYPE, 3), ('uv', DEFAULT_FLOAT_DTYPE, 2), ('normal', DEFAULT_FLOAT_DTYPE, 3), ('colour', DEFAULT_FLOAT_DTYPE, 4)]) vertex_colours = np.ravel(positions) vertex_colours = np.hstack([ np.reshape( np.interp(vertex_colours, (np.min(vertex_colours), np.max(vertex_colours)), (0, 1)), positions.shape), ones([positions.shape[0], 1]) ]) vertices['position'] = positions vertices['uv'] = uvs vertices['normal'] = normals vertices['colour'] = vertex_colours return vertices, faces, outline
PRIMITIVE_METHODS = CaseInsensitiveMapping({ 'Grid': primitive_grid, 'Cube': primitive_cube, }) PRIMITIVE_METHODS.__doc__ = """ Supported geometry primitive generation methods. PRIMITIVE_METHODS : CaseInsensitiveMapping **{'Grid', 'Cube'}** """
[docs]def primitive(method='Cube', **kwargs): """ Returns a geometry primitive using given method. Parameters ---------- method : unicode, optional **{'Cube', 'Grid'}**, Generation method. Other Parameters ---------------- width : numeric, optional {:func:`colour.geometry.primitive_grid_mpl`, :func:`colour.geometry.primitive_cube_mpl`}, Primitive width. height : numeric, optional {:func:`colour.geometry.primitive_grid_mpl`, :func:`colour.geometry.primitive_cube_mpl`}, Primitive height. depth : numeric, optional {:func:`colour.geometry.primitive_grid_mpl`, :func:`colour.geometry.primitive_cube_mpl`}, Primitive depth. width_segments {:func:`colour.geometry.primitive_grid_mpl`, :func:`colour.geometry.primitive_cube_mpl`}, Primitive segments count along the width. height_segments {:func:`colour.geometry.primitive_grid_mpl`, :func:`colour.geometry.primitive_cube_mpl`}, Primitive segments count along the height. depth_segments {:func:`colour.geometry.primitive_grid_mpl`, :func:`colour.geometry.primitive_cube_mpl`}, Primitive segments count along the depth. planes : array_like, optional {:func:`colour.geometry.primitive_cube_mpl`}, **{'-x', '+x', '-y', '+y', '-z', '+z', 'xy', 'xz', 'yz', 'yx', 'zx', 'zy'}**, Included grid primitives in the cube construction. Returns ------- References ---------- :cite:`Cabello2015` Examples -------- >>> vertices, faces, outline = primitive() >>> print(vertices) [([-0.5, 0.5, -0.5], [ 0., 1.], [-0., -0., -1.], [ 0., 1., 0., 1.]) ([ 0.5, 0.5, -0.5], [ 1., 1.], [-0., -0., -1.], [ 1., 1., 0., 1.]) ([-0.5, -0.5, -0.5], [ 0., 0.], [-0., -0., -1.], [ 0., 0., 0., 1.]) ([ 0.5, -0.5, -0.5], [ 1., 0.], [-0., -0., -1.], [ 1., 0., 0., 1.]) ([-0.5, 0.5, 0.5], [ 0., 1.], [ 0., 0., 1.], [ 0., 1., 1., 1.]) ([ 0.5, 0.5, 0.5], [ 1., 1.], [ 0., 0., 1.], [ 1., 1., 1., 1.]) ([-0.5, -0.5, 0.5], [ 0., 0.], [ 0., 0., 1.], [ 0., 0., 1., 1.]) ([ 0.5, -0.5, 0.5], [ 1., 0.], [ 0., 0., 1.], [ 1., 0., 1., 1.]) ([ 0.5, -0.5, -0.5], [ 0., 1.], [-0., -1., -0.], [ 1., 0., 0., 1.]) ([ 0.5, -0.5, 0.5], [ 1., 1.], [-0., -1., -0.], [ 1., 0., 1., 1.]) ([-0.5, -0.5, -0.5], [ 0., 0.], [-0., -1., -0.], [ 0., 0., 0., 1.]) ([-0.5, -0.5, 0.5], [ 1., 0.], [-0., -1., -0.], [ 0., 0., 1., 1.]) ([ 0.5, 0.5, -0.5], [ 0., 1.], [ 0., 1., 0.], [ 1., 1., 0., 1.]) ([ 0.5, 0.5, 0.5], [ 1., 1.], [ 0., 1., 0.], [ 1., 1., 1., 1.]) ([-0.5, 0.5, -0.5], [ 0., 0.], [ 0., 1., 0.], [ 0., 1., 0., 1.]) ([-0.5, 0.5, 0.5], [ 1., 0.], [ 0., 1., 0.], [ 0., 1., 1., 1.]) ([-0.5, -0.5, 0.5], [ 0., 1.], [-1., -0., -0.], [ 0., 0., 1., 1.]) ([-0.5, 0.5, 0.5], [ 1., 1.], [-1., -0., -0.], [ 0., 1., 1., 1.]) ([-0.5, -0.5, -0.5], [ 0., 0.], [-1., -0., -0.], [ 0., 0., 0., 1.]) ([-0.5, 0.5, -0.5], [ 1., 0.], [-1., -0., -0.], [ 0., 1., 0., 1.]) ([ 0.5, -0.5, 0.5], [ 0., 1.], [ 1., 0., 0.], [ 1., 0., 1., 1.]) ([ 0.5, 0.5, 0.5], [ 1., 1.], [ 1., 0., 0.], [ 1., 1., 1., 1.]) ([ 0.5, -0.5, -0.5], [ 0., 0.], [ 1., 0., 0.], [ 1., 0., 0., 1.]) ([ 0.5, 0.5, -0.5], [ 1., 0.], [ 1., 0., 0.], [ 1., 1., 0., 1.])] >>> print(faces) [[ 1 2 0] [ 1 3 2] [ 4 6 5] [ 6 7 5] [ 9 10 8] [ 9 11 10] [12 14 13] [14 15 13] [17 18 16] [17 19 18] [20 22 21] [22 23 21]] >>> print(outline) [[ 0 2] [ 2 3] [ 3 1] [ 1 0] [ 4 6] [ 6 7] [ 7 5] [ 5 4] [ 8 10] [10 11] [11 9] [ 9 8] [12 14] [14 15] [15 13] [13 12] [16 18] [18 19] [19 17] [17 16] [20 22] [22 23] [23 21] [21 20]] >>> vertices, faces, outline = primitive('Grid') >>> print(vertices) [([-0.5, 0.5, 0. ], [ 0., 1.], [ 0., 0., 1.], [ 0., 1., 0., 1.]) ([ 0.5, 0.5, 0. ], [ 1., 1.], [ 0., 0., 1.], [ 1., 1., 0., 1.]) ([-0.5, -0.5, 0. ], [ 0., 0.], [ 0., 0., 1.], [ 0., 0., 0., 1.]) ([ 0.5, -0.5, 0. ], [ 1., 0.], [ 0., 0., 1.], [ 1., 0., 0., 1.])] >>> print(faces) [[0 2 1] [2 3 1]] >>> print(outline) [[0 2] [2 3] [3 1] [1 0]] """ method = validate_method(method, PRIMITIVE_METHODS) function = PRIMITIVE_METHODS[method] return function(**filter_kwargs(function, **kwargs))