-
Notifications
You must be signed in to change notification settings - Fork 182
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
NF: Sphere, Geometry & Material (#946)
NF: add sphere actor w/ materials NF: mesh material NF: geometry & mesh
- Loading branch information
Showing
6 changed files
with
342 additions
and
257 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.