diff --git a/fury/actor.py b/fury/actor.py new file mode 100644 index 000000000..e54b599a0 --- /dev/null +++ b/fury/actor.py @@ -0,0 +1,99 @@ +import numpy as np + +from fury.geometry import buffer_to_geometry, create_mesh +from fury.material import _create_mesh_material +import fury.primitive as fp + + +def sphere( + centers, + colors, + *, + radii=1.0, + phi=16, + theta=16, + opacity=None, + material="phong", + enable_picking=True, +): + """ + Visualize one or many spheres with different colors and radii. + + Parameters + ---------- + centers : ndarray, shape (N, 3) + Spheres positions. + colors : ndarray, shape (N, 3) or (N, 4) or tuple (3,) or tuple (4,) + RGB or RGBA (for opacity) R, G, B, and A should be in the range [0, 1]. + radii : float or ndarray, shape (N,) + Sphere radius. Can be a single value for all spheres or an array of + radii for each sphere. + phi : int, optional + The number of segments in the longitude direction. + theta : int, optional + The number of segments in the latitude direction. + opacity : float, optional + Takes values from 0 (fully transparent) to 1 (opaque). + If both `opacity` and RGBA are provided, the final alpha will be: + final_alpha = alpha_in_RGBA * opacity + material : str, optional + The material type for the spheres. Options are 'phong' and 'basic'. + enable_picking : bool, optional + Whether the spheres should be pickable in a 3D scene. + + Returns + ------- + mesh_actor : Actor + A mesh actor containing the generated spheres, with the specified + material and properties. + + Examples + -------- + >>> from fury import window, actor + >>> scene = window.Scene() + >>> centers = np.random.rand(5, 3) + >>> colors = np.random.rand(5, 3) + >>> sphere_actor = actor.sphere(centers, colors, radii=0.5) + >>> scene.add(sphere_actor) + >>> # window.show(scene) + """ + + scales = radii + directions = (1, 0, 0) + + vertices, faces = fp.prim_sphere(phi=phi, theta=theta) + + res = fp.repeat_primitive( + vertices, + faces, + directions=directions, + centers=centers, + colors=colors, + scales=scales, + ) + big_vertices, big_faces, big_colors, _ = res + + prim_count = len(centers) + + big_colors = big_colors / 255.0 + + if isinstance(opacity, (int, float)): + if big_colors.shape[1] == 3: + big_colors = np.hstack( + (big_colors, np.full((big_colors.shape[0], 1), opacity)) + ) + else: + big_colors[:, 3] *= opacity + + geo = buffer_to_geometry( + indices=big_faces.astype("int32"), + positions=big_vertices.astype("float32"), + texcoords=big_vertices.astype("float32"), + colors=big_colors.astype("float32"), + ) + + mat = _create_mesh_material(material=material, enable_picking=enable_picking) + obj = create_mesh(geometry=geo, material=mat) + obj.local.position = centers[0] + obj.prim_count = prim_count + return obj diff --git a/fury/geometry.py b/fury/geometry.py new file mode 100644 index 000000000..af0b4ed44 --- /dev/null +++ b/fury/geometry.py @@ -0,0 +1,43 @@ +from pygfx import Geometry, Mesh + + +def buffer_to_geometry(positions, **kwargs): + """ + Convert a buffer to a geometry object. + + Parameters + ---------- + positions : array_like + The positions buffer. + kwargs : dict + A dict of attributes to define on the geometry object. Keys can be + "colors", "normals", "texcoords", + "indices", ... + + Returns + ------- + geo : Geometry + The geometry object. + """ + geo = Geometry(positions=positions, **kwargs) + return geo + + +def create_mesh(geometry, material): + """ + Create a mesh object. + + Parameters + ---------- + geometry : Geometry + The geometry object. + material : Material + The material object. + + Returns + ------- + mesh : Mesh + The mesh object. + """ + mesh = Mesh(geometry=geometry, material=material) + return mesh diff --git a/fury/material.py b/fury/material.py new file mode 100644 index 000000000..32e5ebaee --- /dev/null +++ b/fury/material.py @@ -0,0 +1,64 @@ +import pygfx as gfx + + +def _create_mesh_material( + *, material="phong", enable_picking=True, color=None, opacity=1.0, mode="vertex" +): + """ + Create a mesh material. + + Parameters + ---------- + material : str, optional + The type of material to create. Options are 'phong' (default) and + 'basic'. + enable_picking : bool, optional + Whether the material should be pickable in a scene. + color : tuple or None, optional + The color of the material, represented as an RGBA tuple. If None, the + default color is used. + opacity : float, optional + The opacity of the material, from 0 (transparent) to 1 (opaque). + If RGBA is provided, the final alpha will be: + final_alpha = alpha_in_RGBA * opacity + mode : str, optional + The color mode of the material. Options are 'auto' and 'vertex'. + + Returns + ------- + gfx.MeshMaterial + A mesh material object of the specified type with the given properties. + """ + + if not (0 <= opacity <= 1): + raise ValueError("Opacity must be between 0 and 1.") + + if color is None and mode == "auto": + raise ValueError("Color must be specified when mode is 'auto'.") + + elif color is not None: + if len(color) == 3: + color = (*color, opacity) + elif len(color) == 4: + color = color + color[3] *= opacity + else: + raise ValueError("Color must be a tuple of length 3 or 4.") + + if mode == "vertex": + color = (1, 1, 1) + + if material == "phong": + return gfx.MeshPhongMaterial( + pick_write=enable_picking, + color_mode=mode, + color=color, + ) + elif material == "basic": + return gfx.MeshBasicMaterial( + pick_write=enable_picking, + color_mode=mode, + color=color, + ) + else: + raise ValueError(f"Unsupported material type: {material}") diff --git a/fury/utils.py b/fury/utils.py index 87b6b189c..e41314f6b 100644 --- a/fury/utils.py +++ b/fury/utils.py @@ -26,8 +26,7 @@ def map_coordinates_3d_4d(input_array, indices): if input_array.ndim == 4: values_4d = [] for i in range(input_array.shape[-1]): - values_tmp = map_coordinates( - input_array[..., i], indices.T, order=1) + values_tmp = map_coordinates(input_array[..., i], indices.T, order=1) values_4d.append(values_tmp) return np.ascontiguousarray(np.array(values_4d).T) @@ -152,8 +151,7 @@ def get_grid_cells_position(shapes, *, aspect_ratio=16 / 9.0, dim=None): # Use indexing="xy" so the cells are in row-major (C-order). Also, # the Y coordinates are negative so the cells are order from top to bottom. - X, Y, Z = np.meshgrid(np.arange(n_cols), - - np.arange(n_rows), [0], indexing="xy") + X, Y, Z = np.meshgrid(np.arange(n_cols), -np.arange(n_rows), [0], indexing="xy") return cell_shape * np.array([X.flatten(), Y.flatten(), Z.flatten()]).T