diff --git a/CI/unit_tests/mesh/test_cylinder.py b/CI/unit_tests/mesh/test_cylinder.py index 428e47d..0835a94 100644 --- a/CI/unit_tests/mesh/test_cylinder.py +++ b/CI/unit_tests/mesh/test_cylinder.py @@ -75,7 +75,7 @@ def test_build_cylinder(self): ------- Test if a sphere mesh is constructed correctly. """ - cylinder = self.cylinder.create_mesh( + cylinder = self.cylinder.instantiate_mesh( starting_position=np.array([1, 1, 1]), starting_orientation=np.array([1, 1, 1]), ) diff --git a/CI/unit_tests/mesh/test_sphere.py b/CI/unit_tests/mesh/test_sphere.py index 97c736a..70f9acc 100644 --- a/CI/unit_tests/mesh/test_sphere.py +++ b/CI/unit_tests/mesh/test_sphere.py @@ -64,7 +64,7 @@ def test_build_sphere(self): ------- Test if a sphere mesh is constructed correctly. """ - sphere = self.sphere.create_mesh(starting_position=np.array([1, 1, 1])) + sphere = self.sphere.instantiate_mesh(starting_position=np.array([1, 1, 1])) self.assertEqual(sphere.has_vertex_normals(), True) self.assertEqual(type(sphere), o3d.geometry.TriangleMesh) np.testing.assert_almost_equal(sphere.get_center(), [1.0, 1.0, 1.0]) diff --git a/examples/all_shapes.py b/examples/all_shapes.py new file mode 100644 index 0000000..a5dbf6d --- /dev/null +++ b/examples/all_shapes.py @@ -0,0 +1,123 @@ +""" +ZnVis: A Zincwarecode package. +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Tutorial script to visualize simple spheres over a random trajectory. +""" + +import numpy as np + +import znvis as vis + +if __name__ == "__main__": + """ + Run the all shapes example. + """ + + material_1 = vis.Material(colour=np.array([30, 144, 255]) / 255, alpha=0.9) + # Define the sphere. + trajectory = np.random.uniform(-10, 10, (10, 1, 3)) + mesh = vis.Sphere(radius=2.0, material=material_1, resolution=30) + particle = vis.Particle(name="Sphere", mesh=mesh, position=trajectory) + + material_2 = vis.Material(colour=np.array([255, 140, 0]) / 255, alpha=1.0) + # Define the cylinder. + trajectory_2 = np.random.uniform(-10, 10, (10, 1, 3)) + mesh_2 = vis.Cylinder(radius=1.0, + height=2.0, + split=1, + material=material_2, + resolution=30) + particle_2 = vis.Particle(name="Cylinder", mesh=mesh_2, position=trajectory_2) + + material_3 = vis.Material(colour=np.array([100, 255, 130]) / 255, alpha=1.0) + # Define the icosahedron. + trajectory_3 = np.random.uniform(-10, 10, (10, 1, 3)) + mesh_3 = vis.Icosahedron(radius=2.0, material=material_3) + particle_3 = vis.Particle(name="Icosahedron", mesh=mesh_3, position=trajectory_3) + + material_4 = vis.Material(colour=np.array([255, 200, 50]) / 255, alpha=1.0) + # Define the torus. + trajectory_4 = np.random.uniform(-10, 10, (10, 1, 3)) + mesh_4 = vis.Torus(torus_radius=1.0, + tube_radius=0.5, + tubular_resolution=30, + radial_resolution=30, + material=material_4) + particle_4 = vis.Particle(name="Torus", mesh=mesh_4, position=trajectory_4) + + material_5 = vis.Material(colour=np.array([250, 50, 20]) / 255, alpha=1.0) + # Define the mobius loop. + trajectory_5 = np.random.uniform(-10, 10, (10, 1, 3)) + mesh_5 = vis.MobiusLoop(twists=3, + radius=2, + flatness=1, + width=2, scale=1, + length_split=200, + width_split=200, + material=material_5) + particle_5 = vis.Particle(name="MobiusLoop", mesh=mesh_5, position=trajectory_5) + + material_6 = vis.Material(colour=np.array([255, 90, 255]) / 255, alpha=1.0) + # Define the octahedron. + trajectory_6 = np.random.uniform(-10, 10, (10, 1, 3)) + mesh_6 = vis.Octahedron(radius=2.0, material=material_6) + particle_6 = vis.Particle(name="Octahedron", mesh=mesh_6, position=trajectory_6) + + material_7 = vis.Material(colour=np.array([255, 220, 100]) / 255, alpha=1.0) + # Define the tetrahedron. + trajectory_7 = np.random.uniform(-10, 10, (10, 1, 3)) + mesh_7 = vis.Tetrahedron(radius=2.0, material=material_7) + particle_7 = vis.Particle(name="Tetrahedron", mesh=mesh_7, position=trajectory_7) + + material_8 = vis.Material(colour=np.array([255, 200, 240]) / 255, alpha=1.0) + # Define the arrow. + trajectory_8 = np.random.uniform(-10, 10, (10, 1, 3)) + direction_8 = np.random.uniform(-1, 1, (10, 1, 3)) + mesh_8 = vis.Arrow(scale=2, material=material_8, resolution=30) + particle_8 = vis.Particle(name="Arrow", + mesh=mesh_8, + position=trajectory_8, + director=direction_8) + + material_9 = vis.Material(colour=np.array([150, 255, 230]) / 255, alpha=1.0) + # Define the box. + trajectory_9 = np.random.uniform(-10, 10, (10, 1, 3)) + mesh_9 = vis.Box(width=1, height=3, depth=0.5, material=material_9) + particle_9 = vis.Particle(name="BoxMesh", mesh=mesh_9, position=trajectory_9) + + material_10 = vis.Material(colour=np.array([255, 10, 100]) / 255, alpha=1.0) + # Define the cone. + trajectory_10 = np.random.uniform(-10, 10, (10, 1, 3)) + mesh_10 = vis.Cone(radius=1.0, height=2.0, material=material_10, resolution=30) + particle_10 = vis.Particle(name="Cone", mesh=mesh_10, position=trajectory_10) + + particle_list = [particle, particle_2, particle_3, particle_4, particle_5, + particle_6, particle_7, particle_8, particle_9, particle_10] + + # Create a bounding box + bounding_box = vis.BoundingBox( + center=np.array([0, 0, 0]), box_size=np.array([20, 20, 20]) + ) + + # Construct the visualizer and run + visualizer = vis.Visualizer( + particles=particle_list, frame_rate=20, bounding_box=bounding_box + ) + visualizer.run_visualization() \ No newline at end of file diff --git a/examples/dynamic_coloring.py b/examples/dynamic_coloring.py new file mode 100644 index 0000000..3d4c3c0 --- /dev/null +++ b/examples/dynamic_coloring.py @@ -0,0 +1,64 @@ +""" +ZnVis: A Zincwarecode package. +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Tutorial script to visualize simple spheres over a random trajectory. +""" + +import numpy as np + +import znvis as vis + +if __name__ == "__main__": + """ + Run the dynamic color example. + """ + + # Create a color list (N_frames, N_particles, 3 (RGB)) + # Basically give each particle a specified color for each frame + colours = np.tile([30, 144, 255], (100, 5, 1)) + # Change the color of the first particle to red + colours[:, 0, 0] = np.linspace(30, 255, 100) + # Change the color of the second particle to green + colours[:, 1, 1] = np.linspace(144, 255, 100) + colours[:, 1, 2] = np.linspace(255, 30, 100) + # Change the color of the third particle to blue + colours[:, 2, 0] = np.linspace(30, 10, 100) + colours[:, 2, 1] = np.linspace(140, 90, 100) + # Change the color of the fourth particle to white + colours[:, 3, 0] = np.linspace(30, 255, 100) + colours[:, 3, 1] = np.linspace(144, 255, 100) + # Change the color of the fifth particle to black + colours[:, 4, 0] = np.linspace(30, 0, 100) + colours[:, 4, 1] = np.linspace(144, 0, 100) + colours[:, 4, 2] = np.linspace(255, 0, 100) + + material_1 = vis.Material(colour=colours / 255, alpha=1.0) + # Define the first particle. + trajectory = np.random.uniform(-5, 5, (1, 5, 3)) + trajectory = np.tile(trajectory, (100, 1, 1)) + # Turn on dynamic coloring for the mesh + mesh = vis.Sphere(radius=2.0, resolution=20, material=material_1) + particle = vis.Particle( + name="Spheres", mesh=mesh, position=trajectory, smoothing=False + ) + + # Construct the visualizer and run + visualizer = vis.Visualizer(particles=[particle], frame_rate=20) + visualizer.run_visualization() diff --git a/examples/simple_vector_field.py b/examples/simple_vector_field.py new file mode 100644 index 0000000..e473ac3 --- /dev/null +++ b/examples/simple_vector_field.py @@ -0,0 +1,72 @@ +""" +ZnVis: A Zincwarecode package. +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Tutorial script to visualize simple spheres over a random trajectory. +""" + +import numpy as np + +import znvis as vis + +if __name__ == "__main__": + """ + Run the vector field example. + """ + + # Build a grid + x_values = np.linspace(-10, 10, 21) + y_values = np.linspace(-10, 10, 21) + z_values = np.linspace(0, 0, 1) + + grid = np.meshgrid(x_values, y_values, z_values) + grid = np.array(grid).T.reshape(-1, 3) + grid = np.tile(grid, (100, 1, 1)) + + # Define arrow mesh and insert in vector field + material = vis.Material(colour=np.array([30, 144, 255]) / 255, alpha=0.6) + mesh = vis.Arrow(scale=0.5, resolution=20, material=material) + + directions = np.random.uniform(-1, 1, (100, 441, 3)) + # confine the directions to be in the z = 0 plane + directions[:,:,2] = 0 + + vector_field = vis.VectorField(name="VectorField", + mesh=mesh, + position=grid, + direction=directions) + + # Define particles + material_2 = vis.Material(colour=np.array([255, 140, 0]) / 255, alpha=1.0) + mesh_2 = vis.Sphere(radius=1.0, resolution=20, material=material_2) + + trajectory_2 = np.random.uniform(-10, 10, (100, 1, 3)) + # confine the particles to be in the z = 0 plane + trajectory_2[:,:,2] = 0 + + particle = vis.Particle(name="Spheres", + mesh=mesh_2, + position=trajectory_2, + smoothing=False) + + # Construct the visualizer and run + visualizer = vis.Visualizer(particles=[particle], + vector_field=[vector_field], + frame_rate=20) + visualizer.run_visualization() \ No newline at end of file diff --git a/znvis/__init__.py b/znvis/__init__.py index c7688de..71277c7 100644 --- a/znvis/__init__.py +++ b/znvis/__init__.py @@ -29,6 +29,14 @@ from znvis.mesh.custom import CustomMesh from znvis.mesh.cylinder import Cylinder from znvis.mesh.sphere import Sphere +from znvis.mesh.arrow import Arrow +from znvis.mesh.box import Box +from znvis.mesh.cone import Cone +from znvis.mesh.tetrahedron import Tetrahedron +from znvis.mesh.octahedron import Octahedron +from znvis.mesh.icosahedron import Icosahedron +from znvis.mesh.torus import Torus +from znvis.mesh.mobius_loop import MobiusLoop from znvis.particle.particle import Particle from znvis.particle.vector_field import VectorField from znvis.visualizer.visualizer import Visualizer @@ -37,6 +45,13 @@ Particle.__name__, Sphere.__name__, Arrow.__name__, + Box.__name__, + Cone.__name__, + Tetrahedron.__name__, + Octahedron.__name__, + Icosahedron.__name__, + Torus.__name__, + MobiusLoop.__name__, VectorField.__name__, Visualizer.__name__, Cylinder.__name__, diff --git a/znvis/mesh/arrow.py b/znvis/mesh/arrow.py index bb9773c..1da26ff 100644 --- a/znvis/mesh/arrow.py +++ b/znvis/mesh/arrow.py @@ -33,7 +33,8 @@ @dataclass class Arrow(Mesh): """ - A class to produce arrow meshes. + A class to produce arrow meshes. Arrow meshes are a special case and need to + overwrite the instantiate_mesh of the parent mesh class. Attributes ---------- @@ -46,24 +47,27 @@ class Arrow(Mesh): scale: float = 1.0 resolution: int = 10 - def create_mesh( - self, starting_position: np.ndarray, direction: np.ndarray = None + def instantiate_mesh( + self, starting_position: np.ndarray, starting_orientation: np.ndarray = None ) -> o3d.geometry.TriangleMesh: """ - Create a mesh object defined by the dataclass. + Create and correctly orient an arrow mesh. Overwrites the parent class + """ + mesh = self.create_mesh(starting_orientation) + mesh.compute_vertex_normals() + if starting_orientation is not None: + matrix = rotation_matrix(np.array([0, 0, 1]), starting_orientation) + mesh.rotate(matrix, center=(0, 0, 0)) - Parameters - ---------- - starting_position : np.ndarray shape=(3,) - Starting position of the mesh. - direction : np.ndarray shape=(3,) (default = None) - Direction of the mesh. + # Translate the arrow to the starting position and center the origin + mesh.translate(starting_position.astype(float)) - Returns - ------- - mesh : o3d.geometry.TriangleMesh - """ + return mesh + def create_mesh(self, direction: np.ndarray) -> o3d.geometry.TriangleMesh: + """ + Creates an arrow mesh object. + """ direction_length = np.linalg.norm(direction) cylinder_radius = 0.06 * direction_length * self.scale @@ -71,19 +75,10 @@ def create_mesh( cone_radius = 0.15 * direction_length * self.scale cone_height = 0.15 * direction_length * self.scale - arrow = o3d.geometry.TriangleMesh.create_arrow( - cylinder_radius=cylinder_radius, - cylinder_height=cylinder_height, - cone_radius=cone_radius, + return o3d.geometry.TriangleMesh.create_arrow( + cylinder_radius=cylinder_radius, + cylinder_height=cylinder_height, + cone_radius=cone_radius, cone_height=cone_height, resolution=self.resolution, ) - - arrow.compute_vertex_normals() - matrix = rotation_matrix(np.array([0, 0, 1]), direction) - arrow.rotate(matrix, center=(0, 0, 0)) - - # Translate the arrow to the starting position and center the origin - arrow.translate(starting_position.astype(float)) - - return arrow diff --git a/znvis/mesh/box.py b/znvis/mesh/box.py new file mode 100644 index 0000000..dc8a252 --- /dev/null +++ b/znvis/mesh/box.py @@ -0,0 +1,63 @@ +""" +ZnVis: A Zincwarecode package. + +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. + +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ + +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Create a box mesh +""" + +from dataclasses import dataclass + +import numpy as np +import open3d as o3d + +from znvis.transformations.rotation_matrices import rotation_matrix + +from znvis.mesh import Mesh + + +@dataclass +class Box(Mesh): + """ + A class to produce box meshes. + + Attributes + ---------- + width : float + Width of the box. + height : float + Height of the box. + depth : float + Depth of the box. + """ + + width: float = 1.0 + height: float = 1.0 + depth: float = 1.0 + + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_box( + width=self.width, + height=self.height, + depth=self.depth + ) + diff --git a/znvis/mesh/cone.py b/znvis/mesh/cone.py new file mode 100644 index 0000000..241f810 --- /dev/null +++ b/znvis/mesh/cone.py @@ -0,0 +1,67 @@ +""" +ZnVis: A Zincwarecode package. + +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. + +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ + +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Create a cone mesh +""" + +from dataclasses import dataclass + +import numpy as np +import open3d as o3d + +from znvis.transformations.rotation_matrices import rotation_matrix + +from znvis.mesh import Mesh + + +@dataclass +class Cone(Mesh): + """ + A class to produce cone meshes. + + Attributes + ---------- + radius : float + Radius of the cone. + height : float + Height of the cone. + resolution : int + Resolution of the cone. + split : int + The height will be split into this many segments. + """ + + radius: float = 1.0 + height: float = 2.0 + resolution: int = 20 + split: int = 1 + + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_cone( + radius=self.radius, + height=self.height, + resolution=self.resolution, + split=self.split, + ) + diff --git a/znvis/mesh/custom.py b/znvis/mesh/custom.py index ab2711e..826c8af 100644 --- a/znvis/mesh/custom.py +++ b/znvis/mesh/custom.py @@ -31,13 +31,14 @@ from znvis.transformations.rotation_matrices import rotation_matrix -from .mesh import Mesh +from znvis.mesh import Mesh @dataclass class CustomMesh(Mesh): """ - A class to produce cylinder meshes. + A class to produce custom meshes. Custom meshes are special and need to override + the create_mesh method. Attributes ---------- @@ -48,22 +49,11 @@ class CustomMesh(Mesh): file: str = None scale: float = 1.0 - def create_mesh( + def instantiate_mesh( self, starting_position: np.ndarray, starting_orientation: np.ndarray = None ) -> o3d.geometry.TriangleMesh: """ Create a mesh object defined by the dataclass. - - Parameters - ---------- - starting_position : np.ndarray shape=(3,) - Starting position of the mesh. - starting_orientation : np.ndarray shape=(3,) (default = None) - Starting orientation of the mesh. - - Returns - ------- - mesh : o3d.geometry.TriangleMesh """ mesh = o3d.io.read_triangle_mesh(self.file) mesh.compute_vertex_normals() diff --git a/znvis/mesh/cylinder.py b/znvis/mesh/cylinder.py index c644d07..34b77ea 100644 --- a/znvis/mesh/cylinder.py +++ b/znvis/mesh/cylinder.py @@ -31,7 +31,7 @@ from znvis.transformations.rotation_matrices import rotation_matrix -from .mesh import Mesh +from znvis.mesh import Mesh @dataclass @@ -56,33 +56,12 @@ class Cylinder(Mesh): split: int = 1 resolution: int = 10 - def create_mesh( - self, starting_position: np.ndarray, starting_orientation: np.ndarray = None - ) -> o3d.geometry.TriangleMesh: - """ - Create a mesh object defined by the dataclass. - - Parameters - ---------- - starting_position : np.ndarray shape=(3,) - Starting position of the mesh. - starting_orientation : np.ndarray shape=(3,) (default = None) - Starting orientation of the mesh. - - Returns - ------- - mesh : o3d.geometry.TriangleMesh - """ - cylinder = o3d.geometry.TriangleMesh.create_cylinder( + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_cylinder( radius=self.radius, height=self.height, split=self.split, resolution=self.resolution, ) - cylinder.compute_vertex_normals() - cylinder.translate(starting_position.astype(float)) - if starting_orientation is not None: - matrix = rotation_matrix(self.base_direction, starting_orientation) - cylinder.rotate(matrix) - return cylinder diff --git a/znvis/mesh/icosahedron.py b/znvis/mesh/icosahedron.py new file mode 100644 index 0000000..2c7f5d6 --- /dev/null +++ b/znvis/mesh/icosahedron.py @@ -0,0 +1,55 @@ +""" +ZnVis: A Zincwarecode package. + +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. + +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ + +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Create a icosahedron mesh +""" + +from dataclasses import dataclass + +import numpy as np +import open3d as o3d + +from znvis.transformations.rotation_matrices import rotation_matrix + +from znvis.mesh import Mesh + + +@dataclass +class Icosahedron(Mesh): + """ + A class to produce icosahedron meshes. + + Attributes + ---------- + radius : float + Distance from centroid to mesh vertices. + """ + + radius: float = 1.0 + + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_icosahedron( + radius=self.radius + ) + diff --git a/znvis/mesh/mesh.py b/znvis/mesh/mesh.py index 252a4b0..85cb215 100644 --- a/znvis/mesh/mesh.py +++ b/znvis/mesh/mesh.py @@ -27,6 +27,7 @@ import open3d.visualization.rendering as rendering from znvis.material.material import Material +from znvis.transformations.rotation_matrices import rotation_matrix @dataclass @@ -48,7 +49,9 @@ def __post_init__(self): Post init function to create materials. """ material = rendering.MaterialRecord() - material.base_color = np.hstack((self.material.colour, self.material.alpha)) + self.material.colour = np.array(self.material.colour) + if self.material.colour.ndim != 3: + material.base_color = np.hstack((self.material.colour, self.material.alpha)) material.shader = "defaultLitTransparency" material.base_metallic = self.material.metallic material.base_roughness = self.material.roughness @@ -57,7 +60,7 @@ def __post_init__(self): self.o3d_material = material - def create_mesh( + def instantiate_mesh( self, starting_position: np.ndarray, starting_orientation: np.ndarray = None ) -> o3d.geometry.TriangleMesh: """ @@ -74,4 +77,17 @@ def create_mesh( ------- mesh : o3d.geometry.TriangleMesh """ - raise NotImplementedError("Implemented in child class.") + mesh = self.create_mesh() + mesh.compute_vertex_normals() + mesh.translate(starting_position.astype(float)) + if starting_orientation is not None: + matrix = rotation_matrix(self.base_direction, starting_orientation) + mesh.rotate(matrix) + + return mesh + + def create_mesh(self) -> o3d.geometry.TriangleMesh: + """ + Create a mesh object defined by the dataclass. + """ + raise NotImplementedError("Method not implemented.") diff --git a/znvis/mesh/mobius_loop.py b/znvis/mesh/mobius_loop.py new file mode 100644 index 0000000..a4ea238 --- /dev/null +++ b/znvis/mesh/mobius_loop.py @@ -0,0 +1,78 @@ +""" +ZnVis: A Zincwarecode package. + +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. + +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ + +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Create a mobius loop mesh +""" + +from dataclasses import dataclass + +import numpy as np +import open3d as o3d + +from znvis.transformations.rotation_matrices import rotation_matrix + +from znvis.mesh import Mesh + + +@dataclass +class MobiusLoop(Mesh): + """ + A class to produce mobius loop meshes. + + Attributes + ---------- + length_split : int + The number of segments along the Mobius strip. + width_split : int + The number of segments along the width of the Mobius strip. + twists : int + Number of twists of the Mobius strip. + radius : float + Radius of the Mobius strip. + flatness : float + Controls the flatness/height of the Mobius strip. + width : float + Width of the Mobius strip. + scale : float + Scale the complete Mobius strip. + """ + + length_split: int = 70 + width_split: int = 15 + twists: int = 1 + radius: float = 1 + flatness: float = 1 + width: float = 1 + scale: float = 1 + + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_mobius( + length_split=self.length_split, + width_split=self.width_split, + twists=self.twists, + raidus=self.radius, # typo in open3d + flatness=self.flatness, + width=self.width, + scale=self.scale, + ) diff --git a/znvis/mesh/octahedron.py b/znvis/mesh/octahedron.py new file mode 100644 index 0000000..6abc8ae --- /dev/null +++ b/znvis/mesh/octahedron.py @@ -0,0 +1,55 @@ +""" +ZnVis: A Zincwarecode package. + +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. + +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ + +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Create a octahedron mesh +""" + +from dataclasses import dataclass + +import numpy as np +import open3d as o3d + +from znvis.transformations.rotation_matrices import rotation_matrix + +from znvis.mesh import Mesh + + +@dataclass +class Octahedron(Mesh): + """ + A class to produce octahedron meshes. + + Attributes + ---------- + radius : float + Distance from centroid to mesh vertices. + """ + + radius: float = 1.0 + + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_octahedron( + radius=self.radius + ) + diff --git a/znvis/mesh/sphere.py b/znvis/mesh/sphere.py index 769503a..a1fda80 100644 --- a/znvis/mesh/sphere.py +++ b/znvis/mesh/sphere.py @@ -28,7 +28,7 @@ from znvis.transformations.rotation_matrices import rotation_matrix -from .mesh import Mesh +from znvis.mesh import Mesh @dataclass @@ -47,30 +47,8 @@ class Sphere(Mesh): radius: float = 1.0 resolution: int = 10 - def create_mesh( - self, starting_position: np.ndarray, starting_orientation: np.ndarray = None - ) -> o3d.geometry.TriangleMesh: - """ - Create a mesh object defined by the dataclass. - - Parameters - ---------- - starting_position : np.ndarray shape=(3,) - Starting position of the mesh. - starting_orientation : np.ndarray shape=(3,) (default = None) - Starting orientation of the mesh. - - Returns - ------- - mesh : o3d.geometry.TriangleMesh - """ - sphere = o3d.geometry.TriangleMesh.create_sphere( + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_sphere( radius=self.radius, resolution=self.resolution ) - sphere.compute_vertex_normals() - sphere.translate(starting_position.astype(float)) - if starting_orientation is not None: - matrix = rotation_matrix(self.base_direction, starting_orientation) - sphere.rotate(matrix) - - return sphere diff --git a/znvis/mesh/tetrahedron.py b/znvis/mesh/tetrahedron.py new file mode 100644 index 0000000..4f9ff5b --- /dev/null +++ b/znvis/mesh/tetrahedron.py @@ -0,0 +1,54 @@ +""" +ZnVis: A Zincwarecode package. + +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. + +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ + +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Create a tetrahedron mesh +""" + +from dataclasses import dataclass + +import numpy as np +import open3d as o3d + +from znvis.transformations.rotation_matrices import rotation_matrix + +from .mesh import Mesh + + +@dataclass +class Tetrahedron(Mesh): + """ + A class to produce tetrahedron meshes. + + Attributes + ---------- + radius : float + Distance from centroid to mesh vertices. + """ + + radius: float = 1.0 + + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_tetrahedron( + radius=self.radius + ) diff --git a/znvis/mesh/torus.py b/znvis/mesh/torus.py new file mode 100644 index 0000000..71ecb1b --- /dev/null +++ b/znvis/mesh/torus.py @@ -0,0 +1,67 @@ +""" +ZnVis: A Zincwarecode package. + +License +------- +This program and the accompanying materials are made available under the terms +of the Eclipse Public License v2.0 which accompanies this distribution, and is +available at https://www.eclipse.org/legal/epl-v20.html +SPDX-License-Identifier: EPL-2.0 +Copyright Contributors to the Zincwarecode Project. + +Contact Information +------------------- +email: zincwarecode@gmail.com +github: https://github.com/zincware +web: https://zincwarecode.com/ + +Citation +-------- +If you use this module please cite us with: + +Summary +------- +Create a torus mesh +""" + +from dataclasses import dataclass + +import numpy as np +import open3d as o3d + +from znvis.transformations.rotation_matrices import rotation_matrix + +from znvis.mesh import Mesh + + +@dataclass +class Torus(Mesh): + """ + A class to produce torus meshes. + + Attributes + ---------- + torus_radius : float + The radius from the center of the torus to the center of the tube. + tube_radius : float + The radius of the torus tube. + tubular_resolution : int + The number of segments along the tubular direction. + radial_resolution : int + The number of segments along the radial direction. + """ + + torus_radius: float = 1.0 + tube_radius: float = 0.3 + tubular_resolution: int = 20 + radial_resolution: int = 30 + + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_torus( + torus_radius=self.torus_radius, + tube_radius=self.tube_radius, + tubular_resolution=self.tubular_resolution, + radial_resolution=self.radial_resolution, + ) + diff --git a/znvis/particle/particle.py b/znvis/particle/particle.py index 3540880..b0234c2 100644 --- a/znvis/particle/particle.py +++ b/znvis/particle/particle.py @@ -51,6 +51,10 @@ class Particle: Director tensor of the shape (n_confs, n_particles, n_dims) mesh_list : list A list of mesh objects, one for each time step. + static : bool (default=False) + If true, only render the mesh once at initialization. Be careful + as this changes the shape of the required position and director + to (n_particles, n_dims) smoothing : bool (default=False) If true, apply smoothing to each mesh object as it is rendered. This will slow down the initial construction of the mesh objects @@ -64,10 +68,10 @@ class Particle: force: np.ndarray = None director: np.ndarray = None mesh_list: typing.List[Mesh] = None - + static: bool = False smoothing: bool = False - def _create_mesh(self, position, director): + def _create_mesh(self, position, director, time_step=None, index=None): """ Create a mesh object for the particle. @@ -84,13 +88,17 @@ def _create_mesh(self, position, director): A mesh object """ if director is not None: - mesh = self.mesh.create_mesh(position, starting_orientation=director) + mesh = self.mesh.instantiate_mesh(position, starting_orientation=director) else: - mesh = self.mesh.create_mesh(position) + mesh = self.mesh.instantiate_mesh(position) + if self.smoothing: - return mesh.filter_smooth_taubin(100) - else: - return mesh + mesh = mesh.filter_smooth_taubin(100) + + if self.mesh.material.colour.ndim == 3: + mesh.paint_uniform_color(self.mesh.material.colour[time_step, index, :]) + + return mesh def construct_mesh_list(self): """ @@ -105,26 +113,34 @@ def construct_mesh_list(self): """ self.mesh_list = [] try: - # n_particles = int(self.position.shape[1]) - n_time_steps = int(len(self.position)) + if not self.static: + n_particles = int(self.position.shape[1]) + n_time_steps = int(self.position.shape[0]) + else: + n_particles = int(self.position.shape[0]) + n_time_steps = 1 + self.position = self.position[np.newaxis, :, :] + if self.director is not None: + self.director = self.director[np.newaxis, :, :] + except ValueError: raise ValueError("There is no data for these particles.") for i in track(range(n_time_steps), description=f"Building {self.name} Mesh"): - for j in range(np.shape(self.position[i])[0]): + for j in range(n_particles): if j == 0: if self.director is not None: mesh = self._create_mesh( - self.position[i][j], self.director[i][j] + self.position[i][j], self.director[i][j], i, j ) else: - mesh = self._create_mesh(self.position[i][j], None) + mesh = self._create_mesh(self.position[i][j], None, i, j) else: if self.director is not None: mesh += self._create_mesh( - self.position[i][j], self.director[i][j] + self.position[i][j], self.director[i][j], i, j ) else: - mesh += self._create_mesh(self.position[i][j], None) + mesh += self._create_mesh(self.position[i][j], None, i, j) self.mesh_list.append(mesh) diff --git a/znvis/particle/vector_field.py b/znvis/particle/vector_field.py index ae8352b..a5f746f 100644 --- a/znvis/particle/vector_field.py +++ b/znvis/particle/vector_field.py @@ -47,6 +47,10 @@ class VectorField: Direction tensor of the shape (n_steps, n_vectors, n_dims) mesh_list : list A list of mesh objects, one for each time step. + static : bool (default=False) + If true, only render the mesh once at initialization. Be careful + as this changes the shape of the required position and direction + to (n_particles, n_dims) smoothing : bool (default=False) If true, apply smoothing to each mesh object as it is rendered. This will slow down the initial construction of the mesh objects @@ -58,10 +62,12 @@ class VectorField: position: np.ndarray = None direction: np.ndarray = None mesh_list: typing.List[Arrow] = None - + static: bool = False smoothing: bool = False - def _create_mesh(self, position: np.ndarray, direction: np.ndarray): + def _create_mesh( + self, position: np.ndarray, direction: np.ndarray, time_step: int, index: int + ): """ Create a mesh object for the vector field. @@ -78,11 +84,16 @@ def _create_mesh(self, position: np.ndarray, direction: np.ndarray): A mesh object """ - mesh = self.mesh.create_mesh(position, direction) + mesh = self.mesh.instantiate_mesh(position, direction) if self.smoothing: - return mesh.filter_smooth_taubin(100) - else: - return mesh + mesh = mesh.filter_smooth_taubin(100) + + if self.mesh.material.colour.ndim == 3: + mesh = mesh.paint_uniform_color( + self.mesh.material.colour[time_step, index, :] + ) + + return mesh def construct_mesh_list(self): """ @@ -97,15 +108,35 @@ def construct_mesh_list(self): """ self.mesh_list = [] try: - n_particles = int(self.position.shape[1]) - n_time_steps = int(self.position.shape[0]) + if not self.static: + n_particles = int(self.position.shape[1]) + n_time_steps = int(self.position.shape[0]) + else: + n_particles = int(self.position.shape[0]) + n_time_steps = 1 + self.position = self.position[np.newaxis, :, :] + self.direction = self.direction[np.newaxis, :, :] + except ValueError: raise ValueError("There is no data for this vector field.") + new_mesh = True + for i in track(range(n_time_steps), description=f"Building {self.name} Mesh"): for j in range(n_particles): - if j == 0: - mesh = self._create_mesh(self.position[i][j], self.direction[i][j]) - else: - mesh += self._create_mesh(self.position[i][j], self.direction[i][j]) + if ( + np.max(np.abs(self.direction[i][j])) > 0 + ): # ignore vectors with length zero + if new_mesh is True: + mesh = self._create_mesh( + self.position[i][j], self.direction[i][j], i, j + ) + new_mesh = False + else: + mesh += self._create_mesh( + self.position[i][j], self.direction[i][j], i, j + ) + + new_mesh = True + self.mesh_list.append(mesh) diff --git a/znvis/rendering/mitsuba.py b/znvis/rendering/mitsuba.py index 8713ad0..6bb14cb 100644 --- a/znvis/rendering/mitsuba.py +++ b/znvis/rendering/mitsuba.py @@ -114,6 +114,8 @@ def render_mesh_objects( self, mesh_objects: dict, view_matrix: np.ndarray, + resolution: list, + samples_per_pixel: int, save_dir: str = "./", save_name: str = "znvis_render.exr", ): @@ -126,6 +128,10 @@ def render_mesh_objects( List of mesh objects to render. view_matrix : np.ndarray View matrix for the camera from open3d. + resolution : list + Resolution of the exported image/video. + samples_per_pixel : int + Sample count per pixel for the sampler. save_dir : str (default = "./") Directory to save the rendered image. save_name : str (default = "znvis_render.exr") @@ -167,6 +173,11 @@ def render_mesh_objects( self.scene_dict[mesh_name] = mitsuba_mesh # Render the scene. + self.scene_dict["sensor"]["thefilm"]["width"] = resolution[0] + self.scene_dict["sensor"]["thefilm"]["height"] = resolution[1] + + self.scene_dict["sensor"]["thesampler"]["sample_count"] = samples_per_pixel + scene = mi.load_dict(self.scene_dict) img = mi.render(scene) diff --git a/znvis/visualizer/visualizer.py b/znvis/visualizer/visualizer.py index 8ff733e..41ab1f7 100644 --- a/znvis/visualizer/visualizer.py +++ b/znvis/visualizer/visualizer.py @@ -68,6 +68,8 @@ def __init__( keep_frames: bool = True, bounding_box: znvis.BoundingBox = None, video_format: str = "mp4", + renderer_resolution: list = [4096, 2160], + renderer_spp: int = 64, renderer: Mitsuba = Mitsuba(), ): """ @@ -88,6 +90,10 @@ def __init__( after combining them into a video. video_format : str The format of the video to be generated. + renderer_resolution : list + List containing the resolution of the rendered videos and screenshots + renderer_spp : int + Samples per pixel for the rendered videos and screenshots. """ self.particles = particles self.vector_field = vector_field @@ -95,12 +101,17 @@ def __init__( self.bounding_box = bounding_box() if bounding_box else None if number_of_steps is None: - number_of_steps = len(particles[0].position) - self.number_of_steps = number_of_steps + len_list = [] + for particle in particles: + if not particle.static: + len_list.append(len(particle.position)) + self.number_of_steps = min(len_list) self.output_folder = pathlib.Path(output_folder).resolve() self.frame_folder = self.output_folder / "video_frames" self.video_format = video_format + self.renderer_resolution = renderer_resolution + self.renderer_spp = renderer_spp self.keep_frames = keep_frames self.renderer = renderer @@ -271,23 +282,44 @@ def _take_screenshot(self, vis): old_state = self.interrupt # get old state self.interrupt = 0 # stop live feed if running. mesh_dict = {} - mesh_center = [] + + if self.vector_field is not None: + for item in self.vector_field: + if item.static: + mesh_dict[item.name] = { + "mesh": item.mesh_list[0], + "bsdf": item.mesh.material.mitsuba_bsdf, + "material": item.mesh.o3d_material, + } + else: + mesh_dict[item.name] = { + "mesh": item.mesh_list[self.counter], + "bsdf": item.mesh.material.mitsuba_bsdf, + "material": item.mesh.o3d_material, + } + for item in self.particles: - mesh_dict[item.name] = { - "mesh": item.mesh_list[self.counter], - "bsdf": item.mesh.material.mitsuba_bsdf, - "material": item.mesh.o3d_material, + if item.static: + mesh_dict[item.name] = { + "mesh": item.mesh_list[0], + "bsdf": item.mesh.material.mitsuba_bsdf, + "material": item.mesh.o3d_material, } - mesh_center.append( - item.mesh_list[self.counter] - .get_axis_aligned_bounding_box() - .get_center() - ) + else: + mesh_dict[item.name] = { + "mesh": item.mesh_list[self.counter], + "bsdf": item.mesh.material.mitsuba_bsdf, + "material": item.mesh.o3d_material, + } view_matrix = vis.scene.camera.get_view_matrix() self.renderer.render_mesh_objects( - mesh_dict, view_matrix, save_name=f"frame_{self.counter}.png" + mesh_dict, + view_matrix, + save_name=f"frame_{self.counter}.png", + resolution=self.renderer_resolution, + samples_per_pixel=self.renderer_spp ) # Restart live feed if it was running before the export. @@ -346,10 +378,11 @@ def _draw_particles(self, visualizer=None, initial: bool = False): visualizer.add_geometry("Box", self.bounding_box) else: for i, item in enumerate(self.particles): - visualizer.remove_geometry(item.name) - visualizer.add_geometry( - item.name, item.mesh_list[self.counter], item.mesh.o3d_material - ) + if not item.static: + visualizer.remove_geometry(item.name) + visualizer.add_geometry( + item.name, item.mesh_list[self.counter], item.mesh.o3d_material + ) def _draw_vector_field(self, visualizer=None, initial: bool = False): """ @@ -375,10 +408,11 @@ def _draw_vector_field(self, visualizer=None, initial: bool = False): ) else: for i, item in enumerate(self.vector_field): - visualizer.remove_geometry(item.name) - visualizer.add_geometry( - item.name, item.mesh_list[self.counter], item.mesh.o3d_material - ) + if not item.static: + visualizer.remove_geometry(item.name) + visualizer.add_geometry( + item.name, item.mesh_list[self.counter], item.mesh.o3d_material + ) def _continuous_trajectory(self, vis): """ @@ -414,12 +448,34 @@ def save_callable(): """ mesh_dict = {} + if self.vector_field is not None: + for item in self.vector_field: + if item.static: + mesh_dict[item.name] = { + "mesh": item.mesh_list[0], + "bsdf": item.mesh.material.mitsuba_bsdf, + "material": item.mesh.o3d_material, + } + else: + mesh_dict[item.name] = { + "mesh": item.mesh_list[self.counter], + "bsdf": item.mesh.material.mitsuba_bsdf, + "material": item.mesh.o3d_material, + } + for item in self.particles: - mesh_dict[item.name] = { - "mesh": item.mesh_list[self.counter], - "bsdf": item.mesh.material.mitsuba_bsdf, - "material": item.mesh.o3d_material, + if item.static: + mesh_dict[item.name] = { + "mesh": item.mesh_list[0], + "bsdf": item.mesh.material.mitsuba_bsdf, + "material": item.mesh.o3d_material, } + else: + mesh_dict[item.name] = { + "mesh": item.mesh_list[self.counter], + "bsdf": item.mesh.material.mitsuba_bsdf, + "material": item.mesh.o3d_material, + } view_matrix = self.vis.scene.camera.get_view_matrix() self.renderer.render_mesh_objects( @@ -427,6 +483,8 @@ def save_callable(): view_matrix, save_dir=self.frame_folder, save_name=f"frame_{self.counter:0>6}.png", + resolution=self.renderer_resolution, + samples_per_pixel=self.renderer_spp ) self.save_thread_finished = True @@ -471,10 +529,16 @@ def save_callable(): Function to be called on thread to save image. """ for i, item in enumerate(self.particles): - if i == 0: - mesh = item.mesh_list[self.counter] + if item.static: + if i == 0: + mesh = item.mesh_list[0] + else: + mesh += item.mesh_list[0] else: - mesh += item.mesh_list[self.counter] + if i == 0: + mesh = item.mesh_list[self.counter] + else: + mesh += item.mesh_list[self.counter] o3d.io.write_triangle_mesh( (self.obj_folder / f"export_mesh_{self.counter}.ply").as_posix(), mesh