Skip to content

Commit

Permalink
NF: Sphere, Geometry & Material (#946)
Browse files Browse the repository at this point in the history
NF: add sphere actor w/ materials
NF: mesh material
NF: geometry & mesh
  • Loading branch information
m-agour authored Dec 20, 2024
1 parent c57b270 commit bfbedf7
Show file tree
Hide file tree
Showing 6 changed files with 342 additions and 257 deletions.
102 changes: 102 additions & 0 deletions fury/actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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,), optional
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
>>> import numpy as np
>>> scene = window.Scene()
>>> centers = np.random.rand(5, 3) * 10
>>> colors = np.random.rand(5, 3)
>>> radii = np.random.rand(5)
>>> sphere_actor = actor.sphere(centers=centers, colors=colors, radii=radii)
>>> scene.add(sphere_actor)
>>> show_manager = window.ShowManager(scene=scene, size=(600, 600))
>>> show_manager.start()
"""

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
43 changes: 43 additions & 0 deletions fury/geometry.py
Original file line number Diff line number Diff line change
@@ -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
64 changes: 64 additions & 0 deletions fury/material.py
Original file line number Diff line number Diff line change
@@ -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 = (*color[:3], 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}")
74 changes: 74 additions & 0 deletions fury/tests/test_actor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from PIL import Image
import numpy as np
import numpy.testing as npt

from fury import actor, window


def test_sphere():
scene = window.Scene()
centers = np.array([[0, 0, 0]])
colors = np.array([[1, 0, 0]])
radii = np.array([1])

sphere_actor = actor.sphere(centers=centers, colors=colors, radii=radii)
scene.add(sphere_actor)

window.record(scene=scene, fname="sphere_test_1.png")

img = Image.open("sphere_test_1.png")
img_array = np.array(img)

mean_r, mean_g, mean_b, _ = np.mean(
img_array.reshape(-1, img_array.shape[2]), axis=0
)

assert mean_r > mean_b and mean_r > mean_g
assert 0 <= mean_r <= 255 and 0 <= mean_g <= 255 and 0 <= mean_b <= 255

npt.assert_array_equal(sphere_actor.local.position, centers[0])

assert sphere_actor.prim_count == 1

center_pixel = img_array[img_array.shape[0] // 2, img_array.shape[1] // 2]
npt.assert_array_equal(center_pixel[0], colors[0][0] * 255)

phi, theta = 100, 100
sphere_actor_2 = actor.sphere(
centers=centers,
colors=colors,
radii=radii,
opacity=1,
material="basic",
phi=phi,
theta=theta,
)
scene.remove(sphere_actor)
scene.add(sphere_actor_2)

window.record(scene=scene, fname="sphere_test_2.png")

img = Image.open("sphere_test_2.png")
img_array = np.array(img)

mean_r, mean_g, mean_b, mean_a = np.mean(
img_array.reshape(-1, img_array.shape[2]), axis=0
)

assert mean_r > mean_b and mean_r > mean_g
assert 0 <= mean_r <= 255 and 0 <= mean_g <= 255 and 0 <= mean_b <= 255
assert mean_a == 255.0
assert mean_b == 0.0
assert mean_g == 0.0

vertices = sphere_actor_2.geometry.positions.view
faces = sphere_actor_2.geometry.indices.view
colors = sphere_actor_2.geometry.colors.view

vertices_mean = np.mean(vertices, axis=0)

npt.assert_array_almost_equal(vertices_mean, centers[0])

assert len(vertices) == len(colors)

npt.assert_array_almost_equal(len(faces), (2 * phi * (theta - 2)))
32 changes: 32 additions & 0 deletions fury/tests/test_geometry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import numpy as np
import numpy.testing as npt

from fury import geometry, material


def test_buffer_to_geometry():
positions = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]]).astype("float32")
geo = geometry.buffer_to_geometry(positions)
npt.assert_array_equal(geo.positions.view, positions)

normals = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]]).astype("float32")
colors = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]]).astype("float32")
indices = np.array([[0, 1, 2]]).astype("int32")
geo = geometry.buffer_to_geometry(
positions, colors=colors, normals=normals, indices=indices
)

npt.assert_array_equal(geo.colors.view, colors)
npt.assert_array_equal(geo.normals.view, normals)
npt.assert_array_equal(geo.indices.view, indices)


def test_create_mesh():
positions = np.array([[0, 0, 0], [1, 1, 1], [2, 2, 2]]).astype("float32")
geo = geometry.buffer_to_geometry(positions)
mat = material._create_mesh_material(
material="phong", color=(1, 0, 0), opacity=0.5, mode="auto"
)
mesh = geometry.create_mesh(geometry=geo, material=mat)
assert mesh.geometry == geo
assert mesh.material == mat
Loading

0 comments on commit bfbedf7

Please sign in to comment.