Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NF: Sphere, Geometry & Material #946

Merged
merged 5 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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", ...
skoudoro marked this conversation as resolved.
Show resolved Hide resolved

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.
skoudoro marked this conversation as resolved.
Show resolved Hide resolved

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
skoudoro marked this conversation as resolved.
Show resolved Hide resolved
'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,
)
skoudoro marked this conversation as resolved.
Show resolved Hide resolved
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
Loading