From d4c766093a5d8b0313ee0f58cbb48d09ddb5bb05 Mon Sep 17 00:00:00 2001 From: Mohamed Abouagour Date: Fri, 6 Dec 2024 21:58:01 -0500 Subject: [PATCH 1/3] NF: add sphere actor w/ materials NF: mesh material NF: geometry & mesh NF: added sphere actor RF: init actors BF: fix `sphere` opacity RF: pep8 issues fix & absolute import RF: convert `actor` module into fury/actor.py DOC: updated Sphere&material docstring --- fury/actor.py | 100 +++++++++++++++++++++++++++++++++++++++++++++++ fury/geometry.py | 33 ++++++++++++++++ fury/material.py | 43 ++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 fury/actor.py create mode 100644 fury/geometry.py create mode 100644 fury/material.py diff --git a/fury/actor.py b/fury/actor.py new file mode 100644 index 000000000..0d67cc2f8 --- /dev/null +++ b/fury/actor.py @@ -0,0 +1,100 @@ +import numpy as np +import fury.primitive as fp +from fury.material import _create_mesh_material +from fury.geometry import buffer_to_geometry, create_mesh + + +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. Default is 16. + theta : int, optional + The number of segments in the latitude direction. Default is 16. + opacity : float, optional + Takes values from 0 (fully transparent) to 1 (opaque). Default is None + (fully opaque). + material : str, optional + The material type for the spheres. Options are 'phong' (default) + and 'basic'. + enable_picking : bool, optional + Whether the spheres should be pickable in a 3D scene. Defaults to True. + + 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. + + 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..0645a95cc --- /dev/null +++ b/fury/geometry.py @@ -0,0 +1,33 @@ +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", ... + """ + 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. + """ + mesh = Mesh(geometry=geometry, material=material) + return mesh diff --git a/fury/material.py b/fury/material.py new file mode 100644 index 000000000..c6e822368 --- /dev/null +++ b/fury/material.py @@ -0,0 +1,43 @@ +import pygfx as gfx + + +def _create_mesh_material( + + material='phong', + enable_picking=True, + color=None, + opacity=1.0): + """ + Create a mesh material. + + Parameters + ---------- + material : str + The type of material to create. Options are 'phong' (default) and + 'basic'. + enable_picking : bool + Whether the material should be pickable in a scene. Defaults to True. + color : tuple or None + The color of the material, represented as an RGBA tuple. If None, the + default color is used. Defaults to None. + opacity : float + The opacity of the material, from 0 (transparent) to 1 (opaque). + Defaults to 1.0. + + Returns + ------- + gfx.MeshMaterial + A mesh material object of the specified type with the given properties. + """ + if material == 'phong': + return gfx.MeshPhongMaterial( + pick_write=enable_picking, + color_mode='vertex' if color is None else 'auto', + color=color if color is not None else (1, 1, 1, opacity), + ) + elif material == 'basic': + return gfx.MeshBasicMaterial( + pick_write=enable_picking, + color_mode='vertex' if color is None else 'auto', + color=color if color is not None else (1, 1, 1, opacity), + ) From e76d44945d00bf64f7b42f677575eabeec55aa77 Mon Sep 17 00:00:00 2001 From: Mohamed Abouagour Date: Mon, 16 Dec 2024 10:53:53 -0500 Subject: [PATCH 2/3] DOC: addressed docs review from @skoudoro --- fury/actor.py | 41 ++++++++++++++++++++--------------------- fury/geometry.py | 10 ++++++++++ fury/material.py | 24 ++++++++++++------------ fury/utils.py | 6 ++---- 4 files changed, 44 insertions(+), 37 deletions(-) diff --git a/fury/actor.py b/fury/actor.py index 0d67cc2f8..e54b599a0 100644 --- a/fury/actor.py +++ b/fury/actor.py @@ -1,7 +1,8 @@ import numpy as np -import fury.primitive as fp -from fury.material import _create_mesh_material + from fury.geometry import buffer_to_geometry, create_mesh +from fury.material import _create_mesh_material +import fury.primitive as fp def sphere( @@ -12,8 +13,8 @@ def sphere( phi=16, theta=16, opacity=None, - material='phong', - enable_picking=True + material="phong", + enable_picking=True, ): """ Visualize one or many spheres with different colors and radii. @@ -28,17 +29,17 @@ def sphere( 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. Default is 16. + The number of segments in the longitude direction. theta : int, optional - The number of segments in the latitude direction. Default is 16. + The number of segments in the latitude direction. opacity : float, optional - Takes values from 0 (fully transparent) to 1 (opaque). Default is None - (fully opaque). + 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' (default) - and 'basic'. + 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. Defaults to True. + Whether the spheres should be pickable in a 3D scene. Returns ------- @@ -74,26 +75,24 @@ def sphere( prim_count = len(centers) - big_colors = big_colors / 255. + 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))) + (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'), + 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) + 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 diff --git a/fury/geometry.py b/fury/geometry.py index 0645a95cc..af0b4ed44 100644 --- a/fury/geometry.py +++ b/fury/geometry.py @@ -13,6 +13,11 @@ def buffer_to_geometry(positions, **kwargs): 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 @@ -28,6 +33,11 @@ def create_mesh(geometry, material): 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 index c6e822368..9589ba6f9 100644 --- a/fury/material.py +++ b/fury/material.py @@ -2,27 +2,27 @@ def _create_mesh_material( - - material='phong', - enable_picking=True, - color=None, - opacity=1.0): + *, material="phong", enable_picking=True, color=None, opacity=1.0, mode="vertex" +): """ Create a mesh material. Parameters ---------- - material : str + material : str, optional The type of material to create. Options are 'phong' (default) and 'basic'. - enable_picking : bool - Whether the material should be pickable in a scene. Defaults to True. - color : tuple or None + 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. Defaults to None. - opacity : float + default color is used. + opacity : float, optional The opacity of the material, from 0 (transparent) to 1 (opaque). - Defaults to 1.0. + 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 ------- 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 From 8b0463c41ff20f9c6576ca9fa3dd7b0f3a5c4746 Mon Sep 17 00:00:00 2001 From: Mohamed Abouagour Date: Mon, 16 Dec 2024 10:56:22 -0500 Subject: [PATCH 3/3] RF: restructured the `_create_mesh_material` function --- fury/material.py | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/fury/material.py b/fury/material.py index 9589ba6f9..32e5ebaee 100644 --- a/fury/material.py +++ b/fury/material.py @@ -29,15 +29,36 @@ def _create_mesh_material( gfx.MeshMaterial A mesh material object of the specified type with the given properties. """ - if material == 'phong': + + 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='vertex' if color is None else 'auto', - color=color if color is not None else (1, 1, 1, opacity), + color_mode=mode, + color=color, ) - elif material == 'basic': + elif material == "basic": return gfx.MeshBasicMaterial( pick_write=enable_picking, - color_mode='vertex' if color is None else 'auto', - color=color if color is not None else (1, 1, 1, opacity), + color_mode=mode, + color=color, ) + else: + raise ValueError(f"Unsupported material type: {material}")