From 5463457452d957a16fcf0ef69acaf46b44b68762 Mon Sep 17 00:00:00 2001 From: phohenberger Date: Mon, 3 Jun 2024 14:57:38 +0200 Subject: [PATCH 01/23] Add static rendering --- znvis/particle/particle.py | 20 ++++++++++++++++---- znvis/visualizer/visualizer.py | 16 ++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/znvis/particle/particle.py b/znvis/particle/particle.py index 3540880..d75f9c1 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,7 +68,7 @@ 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): @@ -105,13 +109,21 @@ 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( diff --git a/znvis/visualizer/visualizer.py b/znvis/visualizer/visualizer.py index ddfbe62..ce257df 100644 --- a/znvis/visualizer/visualizer.py +++ b/znvis/visualizer/visualizer.py @@ -346,10 +346,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): @@ -415,6 +416,13 @@ def save_callable(): """ mesh_dict = {} + for item in self.vector_field: + 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], From 8e74a75169663882e6c1c613b10118c4fd1bb2f8 Mon Sep 17 00:00:00 2001 From: phohenberger Date: Mon, 3 Jun 2024 16:53:33 +0200 Subject: [PATCH 02/23] Fix record video and add static vector field --- znvis/particle/vector_field.py | 17 ++++++++++--- znvis/visualizer/visualizer.py | 44 +++++++++++++++++++++++----------- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/znvis/particle/vector_field.py b/znvis/particle/vector_field.py index 871a3b6..a86a6c9 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,7 +62,7 @@ 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): @@ -97,8 +101,15 @@ 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.") diff --git a/znvis/visualizer/visualizer.py b/znvis/visualizer/visualizer.py index ce257df..e7e3a91 100644 --- a/znvis/visualizer/visualizer.py +++ b/znvis/visualizer/visualizer.py @@ -377,10 +377,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): """ @@ -416,19 +417,34 @@ def save_callable(): """ mesh_dict = {} - for item in self.vector_field: - mesh_dict[item.name] = { - "mesh": item.mesh_list[self.counter], - "bsdf": item.mesh.material.mitsuba_bsdf, - "material": item.mesh.o3d_material, - } + 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( From 146f260585ef10a3d0eb2f2ab8d6f50179f59f26 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Tue, 4 Jun 2024 17:58:08 +0200 Subject: [PATCH 03/23] Fix screen shot and remove old code --- znvis/visualizer/visualizer.py | 37 +++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/znvis/visualizer/visualizer.py b/znvis/visualizer/visualizer.py index e7e3a91..b5ed78b 100644 --- a/znvis/visualizer/visualizer.py +++ b/znvis/visualizer/visualizer.py @@ -271,18 +271,35 @@ 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() From bc59c94dd86a58542535eb653c6786d93ab0c5b2 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Tue, 4 Jun 2024 18:02:30 +0200 Subject: [PATCH 04/23] Fix record_mesh_trajectory for static mesh --- znvis/visualizer/visualizer.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/znvis/visualizer/visualizer.py b/znvis/visualizer/visualizer.py index b5ed78b..1ad4996 100644 --- a/znvis/visualizer/visualizer.py +++ b/znvis/visualizer/visualizer.py @@ -513,10 +513,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 From e65befe41b77b7fc72e1f3e0d402592431dcfab5 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Mon, 10 Jun 2024 09:26:16 +0200 Subject: [PATCH 05/23] Fix static_mesh bug Fixes a bug where if you initialize the static mesh first, the number of frames get set to 1 --- znvis/visualizer/visualizer.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/znvis/visualizer/visualizer.py b/znvis/visualizer/visualizer.py index 1ad4996..ebd0954 100644 --- a/znvis/visualizer/visualizer.py +++ b/znvis/visualizer/visualizer.py @@ -95,8 +95,11 @@ 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" From 6d722134831633ce093d00f2ba1aa5e988f9dbba Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Wed, 12 Jun 2024 15:44:37 +0200 Subject: [PATCH 06/23] Add variable resolution and size --- znvis/rendering/mitsuba.py | 11 +++++++++++ znvis/visualizer/visualizer.py | 16 +++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) 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 ddfbe62..684aab9 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 @@ -101,6 +107,8 @@ def __init__( 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 @@ -287,7 +295,11 @@ def _take_screenshot(self, vis): 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, + sample_count=self.renderer_spp ) # Restart live feed if it was running before the export. @@ -428,6 +440,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 From 402b57db48b4a21754e39162dd2ba5923ab0a1f1 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:14:21 +0200 Subject: [PATCH 07/23] Add most open3d base shapes --- znvis/mesh/box.py | 88 +++++++++++++++++++++++++++++++++ znvis/mesh/cone.py | 85 ++++++++++++++++++++++++++++++++ znvis/mesh/icosahedron.py | 76 +++++++++++++++++++++++++++++ znvis/mesh/mobius_loop.py | 100 ++++++++++++++++++++++++++++++++++++++ znvis/mesh/octahedron.py | 76 +++++++++++++++++++++++++++++ znvis/mesh/tetrahedron.py | 76 +++++++++++++++++++++++++++++ znvis/mesh/torus.py | 88 +++++++++++++++++++++++++++++++++ 7 files changed, 589 insertions(+) create mode 100644 znvis/mesh/box.py create mode 100644 znvis/mesh/cone.py create mode 100644 znvis/mesh/icosahedron.py create mode 100644 znvis/mesh/mobius_loop.py create mode 100644 znvis/mesh/octahedron.py create mode 100644 znvis/mesh/tetrahedron.py create mode 100644 znvis/mesh/torus.py diff --git a/znvis/mesh/box.py b/znvis/mesh/box.py new file mode 100644 index 0000000..9882ed4 --- /dev/null +++ b/znvis/mesh/box.py @@ -0,0 +1,88 @@ +""" +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 .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. + resolution : int + Resolution of the box. + """ + + width: float = 1.0 + height: float = 1.0 + depth: 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 + """ + box = o3d.geometry.TriangleMesh.create_box( + width=self.width, + height=self.height, + depth=self.depth, + resolution=self.resolution, + ) + box.compute_vertex_normals() + box.translate(starting_position.astype(float)) + if starting_orientation is not None: + matrix = rotation_matrix(self.base_direction, starting_orientation) + box.rotate(matrix) + + return box diff --git a/znvis/mesh/cone.py b/znvis/mesh/cone.py new file mode 100644 index 0000000..18ea791 --- /dev/null +++ b/znvis/mesh/cone.py @@ -0,0 +1,85 @@ +""" +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 .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, 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 + """ + cone = o3d.geometry.TriangleMesh.create_cone( + radius = self.radius + ) + cone.compute_vertex_normals() + cone.translate(starting_position.astype(float)) + if starting_orientation is not None: + matrix = rotation_matrix(self.base_direction, starting_orientation) + cone.rotate(matrix) + + return cone diff --git a/znvis/mesh/icosahedron.py b/znvis/mesh/icosahedron.py new file mode 100644 index 0000000..0d40954 --- /dev/null +++ b/znvis/mesh/icosahedron.py @@ -0,0 +1,76 @@ +""" +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 .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, 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 + """ + icosahedron = o3d.geometry.TriangleMesh.create_icosahedron( + radius = self.radius + ) + icosahedron.compute_vertex_normals() + icosahedron.translate(starting_position.astype(float)) + if starting_orientation is not None: + matrix = rotation_matrix(self.base_direction, starting_orientation) + icosahedron.rotate(matrix) + + return icosahedron diff --git a/znvis/mesh/mobius_loop.py b/znvis/mesh/mobius_loop.py new file mode 100644 index 0000000..092cc0b --- /dev/null +++ b/znvis/mesh/mobius_loop.py @@ -0,0 +1,100 @@ +""" +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 .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, 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 + """ + mobius = o3d.geometry.TriangleMesh.create_mobius( + length_split=self.length_split, + width_split=self.width_split, + twists=self.twists, + radius=self.radius, + flatness=self.flatness, + width=self.width, + scale=self.scale, + ) + mobius.compute_vertex_normals() + mobius.translate(starting_position.astype(float)) + if starting_orientation is not None: + matrix = rotation_matrix(self.base_direction, starting_orientation) + mobius.rotate(matrix) + + return mobius diff --git a/znvis/mesh/octahedron.py b/znvis/mesh/octahedron.py new file mode 100644 index 0000000..3069d0c --- /dev/null +++ b/znvis/mesh/octahedron.py @@ -0,0 +1,76 @@ +""" +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 .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, 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 + """ + octahedron = o3d.geometry.TriangleMesh.create_octahedron( + radius = self.radius + ) + octahedron.compute_vertex_normals() + octahedron.translate(starting_position.astype(float)) + if starting_orientation is not None: + matrix = rotation_matrix(self.base_direction, starting_orientation) + octahedron.rotate(matrix) + + return octahedron diff --git a/znvis/mesh/tetrahedron.py b/znvis/mesh/tetrahedron.py new file mode 100644 index 0000000..8d03e48 --- /dev/null +++ b/znvis/mesh/tetrahedron.py @@ -0,0 +1,76 @@ +""" +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, 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 + """ + tetrahedron = o3d.geometry.TriangleMesh.create_tetrahedron( + radius = self.radius + ) + tetrahedron.compute_vertex_normals() + tetrahedron.translate(starting_position.astype(float)) + if starting_orientation is not None: + matrix = rotation_matrix(self.base_direction, starting_orientation) + tetrahedron.rotate(matrix) + + return tetrahedron diff --git a/znvis/mesh/torus.py b/znvis/mesh/torus.py new file mode 100644 index 0000000..843cdb3 --- /dev/null +++ b/znvis/mesh/torus.py @@ -0,0 +1,88 @@ +""" +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 .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, 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 + """ + torus = 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, + ) + torus.compute_vertex_normals() + torus.translate(starting_position.astype(float)) + if starting_orientation is not None: + matrix = rotation_matrix(self.base_direction, starting_orientation) + torus.rotate(matrix) + + return torus From 03e4de653f7969ff764b5a4d272151bbc1585507 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:24:20 +0200 Subject: [PATCH 08/23] Add shapes to __init__.py --- znvis/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/znvis/__init__.py b/znvis/__init__.py index 5306b99..7a7c5ad 100644 --- a/znvis/__init__.py +++ b/znvis/__init__.py @@ -29,6 +29,13 @@ 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 +44,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__, From 6027a9fc80b7bedfcd1864a352d954a2d6ace967 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:28:49 +0200 Subject: [PATCH 09/23] Fix box shape --- znvis/mesh/box.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/znvis/mesh/box.py b/znvis/mesh/box.py index 9882ed4..2a09333 100644 --- a/znvis/mesh/box.py +++ b/znvis/mesh/box.py @@ -47,14 +47,11 @@ class Box(Mesh): Height of the box. depth : float Depth of the box. - resolution : int - Resolution of the box. """ width: float = 1.0 height: float = 1.0 depth: float = 1.0 - resolution: int = 10 def create_mesh( self, starting_position: np.ndarray, starting_orientation: np.ndarray = None @@ -77,7 +74,6 @@ def create_mesh( width=self.width, height=self.height, depth=self.depth, - resolution=self.resolution, ) box.compute_vertex_normals() box.translate(starting_position.astype(float)) From 9c4738450795df4dbd968bcd2a3b214a6108b061 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Wed, 12 Jun 2024 23:35:08 +0200 Subject: [PATCH 10/23] Adapt to typo in open3D --- znvis/mesh/mobius_loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/znvis/mesh/mobius_loop.py b/znvis/mesh/mobius_loop.py index 092cc0b..8891805 100644 --- a/znvis/mesh/mobius_loop.py +++ b/znvis/mesh/mobius_loop.py @@ -86,7 +86,7 @@ def create_mesh( length_split=self.length_split, width_split=self.width_split, twists=self.twists, - radius=self.radius, + raidus=self.radius, # typo in open3d flatness=self.flatness, width=self.width, scale=self.scale, From a0ad74b2931590e56432ee1dbcd27d91dbea70c8 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:05:13 +0200 Subject: [PATCH 11/23] Fix spp argument --- znvis/visualizer/visualizer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/znvis/visualizer/visualizer.py b/znvis/visualizer/visualizer.py index 684aab9..151c4b2 100644 --- a/znvis/visualizer/visualizer.py +++ b/znvis/visualizer/visualizer.py @@ -299,7 +299,7 @@ def _take_screenshot(self, vis): view_matrix, save_name=f"frame_{self.counter}.png", resolution=self.renderer_resolution, - sample_count=self.renderer_spp + samples_per_pixel=self.renderer_spp ) # Restart live feed if it was running before the export. From 604f999f92543589a7315b17254c4e2220a49203 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Thu, 13 Jun 2024 11:14:03 +0200 Subject: [PATCH 12/23] Fix typo --- znvis/visualizer/visualizer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/znvis/visualizer/visualizer.py b/znvis/visualizer/visualizer.py index 151c4b2..3d35151 100644 --- a/znvis/visualizer/visualizer.py +++ b/znvis/visualizer/visualizer.py @@ -107,8 +107,8 @@ def __init__( 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.renderer_resolution = renderer_resolution + self.renderer_spp = renderer_spp self.keep_frames = keep_frames self.renderer = renderer From a052fa9113279f4c5d7596cc104a59aad497f08f Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:38:24 +0200 Subject: [PATCH 13/23] Skip vector of length zero --- znvis/particle/vector_field.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/znvis/particle/vector_field.py b/znvis/particle/vector_field.py index a86a6c9..3c1e9a2 100644 --- a/znvis/particle/vector_field.py +++ b/znvis/particle/vector_field.py @@ -113,10 +113,16 @@ def construct_mesh_list(self): 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(self.direction[i][j]) > 0: # ignore vectors with length zero + if new_mesh is False: + 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]) + new_mesh = False + new_mesh = True + self.mesh_list.append(mesh) From b32f67c15775ad47b0eb10fad79ec4bafaf635db Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Thu, 13 Jun 2024 13:46:56 +0200 Subject: [PATCH 14/23] Fix typos and cone parameters --- znvis/mesh/box.py | 2 +- znvis/mesh/cone.py | 6 +++++- znvis/mesh/icosahedron.py | 2 +- znvis/mesh/octahedron.py | 2 +- znvis/mesh/tetrahedron.py | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) diff --git a/znvis/mesh/box.py b/znvis/mesh/box.py index 2a09333..246e2ed 100644 --- a/znvis/mesh/box.py +++ b/znvis/mesh/box.py @@ -73,7 +73,7 @@ def create_mesh( box = o3d.geometry.TriangleMesh.create_box( width=self.width, height=self.height, - depth=self.depth, + depth=self.depth ) box.compute_vertex_normals() box.translate(starting_position.astype(float)) diff --git a/znvis/mesh/cone.py b/znvis/mesh/cone.py index 18ea791..4ecdbe7 100644 --- a/znvis/mesh/cone.py +++ b/znvis/mesh/cone.py @@ -74,7 +74,11 @@ def create_mesh( mesh : o3d.geometry.TriangleMesh """ cone = o3d.geometry.TriangleMesh.create_cone( - radius = self.radius + radius=self.radius, + height=self.height, + resolution=self.resolution, + split=self.split + ) cone.compute_vertex_normals() cone.translate(starting_position.astype(float)) diff --git a/znvis/mesh/icosahedron.py b/znvis/mesh/icosahedron.py index 0d40954..9a971a9 100644 --- a/znvis/mesh/icosahedron.py +++ b/znvis/mesh/icosahedron.py @@ -65,7 +65,7 @@ def create_mesh( mesh : o3d.geometry.TriangleMesh """ icosahedron = o3d.geometry.TriangleMesh.create_icosahedron( - radius = self.radius + radius=self.radius ) icosahedron.compute_vertex_normals() icosahedron.translate(starting_position.astype(float)) diff --git a/znvis/mesh/octahedron.py b/znvis/mesh/octahedron.py index 3069d0c..b2d2642 100644 --- a/znvis/mesh/octahedron.py +++ b/znvis/mesh/octahedron.py @@ -65,7 +65,7 @@ def create_mesh( mesh : o3d.geometry.TriangleMesh """ octahedron = o3d.geometry.TriangleMesh.create_octahedron( - radius = self.radius + radius=self.radius ) octahedron.compute_vertex_normals() octahedron.translate(starting_position.astype(float)) diff --git a/znvis/mesh/tetrahedron.py b/znvis/mesh/tetrahedron.py index 8d03e48..4af2d7b 100644 --- a/znvis/mesh/tetrahedron.py +++ b/znvis/mesh/tetrahedron.py @@ -65,7 +65,7 @@ def create_mesh( mesh : o3d.geometry.TriangleMesh """ tetrahedron = o3d.geometry.TriangleMesh.create_tetrahedron( - radius = self.radius + radius=self.radius ) tetrahedron.compute_vertex_normals() tetrahedron.translate(starting_position.astype(float)) From 18dbbe6dd7384069ce7eed62dfd72af0b8fafde9 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Thu, 20 Jun 2024 13:53:05 +0200 Subject: [PATCH 15/23] Fix bug for negative direction --- znvis/particle/vector_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/znvis/particle/vector_field.py b/znvis/particle/vector_field.py index 3c1e9a2..2ef1465 100644 --- a/znvis/particle/vector_field.py +++ b/znvis/particle/vector_field.py @@ -117,7 +117,7 @@ def construct_mesh_list(self): for i in track(range(n_time_steps), description=f"Building {self.name} Mesh"): for j in range(n_particles): - if np.max(self.direction[i][j]) > 0: # ignore vectors with length zero + if np.max(np.abs(self.direction[i][j])) > 0: # ignore vectors with length zero if new_mesh is False: mesh += self._create_mesh(self.position[i][j], self.direction[i][j]) else: From a0e9b760922e1cbd74934bf5f3e12d84a05c1bda Mon Sep 17 00:00:00 2001 From: phohenberger Date: Fri, 28 Jun 2024 23:25:25 +0200 Subject: [PATCH 16/23] Move mesh creation to parent class --- znvis/mesh/arrow.py | 41 +++++++++++++++++---------------------- znvis/mesh/box.py | 27 +++----------------------- znvis/mesh/cone.py | 30 ++++------------------------ znvis/mesh/custom.py | 16 +++------------ znvis/mesh/cylinder.py | 29 ++++----------------------- znvis/mesh/icosahedron.py | 27 +++----------------------- znvis/mesh/mesh.py | 17 ++++++++++++++-- znvis/mesh/mobius_loop.py | 28 +++----------------------- znvis/mesh/octahedron.py | 29 ++++----------------------- znvis/mesh/sphere.py | 30 ++++------------------------ znvis/mesh/tetrahedron.py | 26 ++----------------------- znvis/mesh/torus.py | 27 +++----------------------- 12 files changed, 66 insertions(+), 261 deletions(-) diff --git a/znvis/mesh/arrow.py b/znvis/mesh/arrow.py index 5f9382c..244b6aa 100644 --- a/znvis/mesh/arrow.py +++ b/znvis/mesh/arrow.py @@ -34,7 +34,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 create_mesh object of the parent mesh class. Attributes ---------- @@ -47,23 +48,26 @@ class Arrow(Mesh): resolution: int = 10 def create_mesh( - self, starting_position: np.ndarray, direction: np.ndarray = None + 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_object(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_object(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( + 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 index 246e2ed..e87bc31 100644 --- a/znvis/mesh/box.py +++ b/znvis/mesh/box.py @@ -53,32 +53,11 @@ class Box(Mesh): height: float = 1.0 depth: float = 1.0 - 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 - """ - box = o3d.geometry.TriangleMesh.create_box( + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_box( width=self.width, height=self.height, depth=self.depth ) - box.compute_vertex_normals() - box.translate(starting_position.astype(float)) - if starting_orientation is not None: - matrix = rotation_matrix(self.base_direction, starting_orientation) - box.rotate(matrix) - return box diff --git a/znvis/mesh/cone.py b/znvis/mesh/cone.py index 4ecdbe7..241f810 100644 --- a/znvis/mesh/cone.py +++ b/znvis/mesh/cone.py @@ -31,7 +31,7 @@ from znvis.transformations.rotation_matrices import rotation_matrix -from .mesh import Mesh +from znvis.mesh import Mesh @dataclass @@ -56,34 +56,12 @@ class Cone(Mesh): resolution: int = 20 split: int = 1 - def create_mesh( - self, starting_position: np.ndarray, starting_orientation: np.ndarray = None - ) -> o3d.geometry.TriangleMesh: - """ - Create a mesh object defined by the dataclass. + def create_mesh(self) -> o3d.geometry.TriangleMesh: - 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 - """ - cone = o3d.geometry.TriangleMesh.create_cone( + return o3d.geometry.TriangleMesh.create_cone( radius=self.radius, height=self.height, resolution=self.resolution, - split=self.split - + split=self.split, ) - cone.compute_vertex_normals() - cone.translate(starting_position.astype(float)) - if starting_orientation is not None: - matrix = rotation_matrix(self.base_direction, starting_orientation) - cone.rotate(matrix) - return cone diff --git a/znvis/mesh/custom.py b/znvis/mesh/custom.py index ab2711e..7be97d0 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 ---------- @@ -53,17 +54,6 @@ def create_mesh( ) -> 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 index 9a971a9..2c7f5d6 100644 --- a/znvis/mesh/icosahedron.py +++ b/znvis/mesh/icosahedron.py @@ -31,7 +31,7 @@ from znvis.transformations.rotation_matrices import rotation_matrix -from .mesh import Mesh +from znvis.mesh import Mesh @dataclass @@ -47,30 +47,9 @@ class Icosahedron(Mesh): radius: float = 1.0 - def create_mesh( - self, starting_position: np.ndarray, starting_orientation: np.ndarray = None - ) -> o3d.geometry.TriangleMesh: - """ - Create a mesh object defined by the dataclass. + def create_mesh(self) -> o3d.geometry.TriangleMesh: - 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 - """ - icosahedron = o3d.geometry.TriangleMesh.create_icosahedron( + return o3d.geometry.TriangleMesh.create_icosahedron( radius=self.radius ) - icosahedron.compute_vertex_normals() - icosahedron.translate(starting_position.astype(float)) - if starting_orientation is not None: - matrix = rotation_matrix(self.base_direction, starting_orientation) - icosahedron.rotate(matrix) - return icosahedron diff --git a/znvis/mesh/mesh.py b/znvis/mesh/mesh.py index 252a4b0..e3305dd 100644 --- a/znvis/mesh/mesh.py +++ b/znvis/mesh/mesh.py @@ -27,7 +27,7 @@ import open3d.visualization.rendering as rendering from znvis.material.material import Material - +from znvis.transformations.rotation_matrices import rotation_matrix @dataclass class Mesh: @@ -74,4 +74,17 @@ def create_mesh( ------- mesh : o3d.geometry.TriangleMesh """ - raise NotImplementedError("Implemented in child class.") + mesh = self.create_mesh_object() + 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_object(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 index 8891805..a4ea238 100644 --- a/znvis/mesh/mobius_loop.py +++ b/znvis/mesh/mobius_loop.py @@ -31,7 +31,7 @@ from znvis.transformations.rotation_matrices import rotation_matrix -from .mesh import Mesh +from znvis.mesh import Mesh @dataclass @@ -65,24 +65,9 @@ class MobiusLoop(Mesh): width: float = 1 scale: float = 1 - def create_mesh( - self, starting_position: np.ndarray, starting_orientation: np.ndarray = None - ) -> o3d.geometry.TriangleMesh: - """ - Create a mesh object defined by the dataclass. + def create_mesh(self) -> o3d.geometry.TriangleMesh: - 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 - """ - mobius = o3d.geometry.TriangleMesh.create_mobius( + return o3d.geometry.TriangleMesh.create_mobius( length_split=self.length_split, width_split=self.width_split, twists=self.twists, @@ -91,10 +76,3 @@ def create_mesh( width=self.width, scale=self.scale, ) - mobius.compute_vertex_normals() - mobius.translate(starting_position.astype(float)) - if starting_orientation is not None: - matrix = rotation_matrix(self.base_direction, starting_orientation) - mobius.rotate(matrix) - - return mobius diff --git a/znvis/mesh/octahedron.py b/znvis/mesh/octahedron.py index b2d2642..6abc8ae 100644 --- a/znvis/mesh/octahedron.py +++ b/znvis/mesh/octahedron.py @@ -31,7 +31,7 @@ from znvis.transformations.rotation_matrices import rotation_matrix -from .mesh import Mesh +from znvis.mesh import Mesh @dataclass @@ -47,30 +47,9 @@ class Octahedron(Mesh): radius: float = 1.0 - 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 - """ - octahedron = o3d.geometry.TriangleMesh.create_octahedron( + def create_mesh(self) -> o3d.geometry.TriangleMesh: + + return o3d.geometry.TriangleMesh.create_octahedron( radius=self.radius ) - octahedron.compute_vertex_normals() - octahedron.translate(starting_position.astype(float)) - if starting_orientation is not None: - matrix = rotation_matrix(self.base_direction, starting_orientation) - octahedron.rotate(matrix) - return octahedron diff --git a/znvis/mesh/sphere.py b/znvis/mesh/sphere.py index 769503a..67a829d 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_object(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 index 4af2d7b..4f9ff5b 100644 --- a/znvis/mesh/tetrahedron.py +++ b/znvis/mesh/tetrahedron.py @@ -47,30 +47,8 @@ class Tetrahedron(Mesh): radius: float = 1.0 - def create_mesh( - self, starting_position: np.ndarray, starting_orientation: np.ndarray = None - ) -> o3d.geometry.TriangleMesh: - """ - Create a mesh object defined by the dataclass. + def create_mesh(self) -> o3d.geometry.TriangleMesh: - 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 - """ - tetrahedron = o3d.geometry.TriangleMesh.create_tetrahedron( + return o3d.geometry.TriangleMesh.create_tetrahedron( radius=self.radius ) - tetrahedron.compute_vertex_normals() - tetrahedron.translate(starting_position.astype(float)) - if starting_orientation is not None: - matrix = rotation_matrix(self.base_direction, starting_orientation) - tetrahedron.rotate(matrix) - - return tetrahedron diff --git a/znvis/mesh/torus.py b/znvis/mesh/torus.py index 843cdb3..71ecb1b 100644 --- a/znvis/mesh/torus.py +++ b/znvis/mesh/torus.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 Torus(Mesh): tubular_resolution: int = 20 radial_resolution: int = 30 - def create_mesh( - self, starting_position: np.ndarray, starting_orientation: np.ndarray = None - ) -> o3d.geometry.TriangleMesh: - """ - Create a mesh object defined by the dataclass. + def create_mesh(self) -> o3d.geometry.TriangleMesh: - 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 - """ - torus = o3d.geometry.TriangleMesh.create_torus( + 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, ) - torus.compute_vertex_normals() - torus.translate(starting_position.astype(float)) - if starting_orientation is not None: - matrix = rotation_matrix(self.base_direction, starting_orientation) - torus.rotate(matrix) - return torus From 74c45470f41fa9b251ecbd08613f4ad038157047 Mon Sep 17 00:00:00 2001 From: phohenberger Date: Fri, 28 Jun 2024 23:27:19 +0200 Subject: [PATCH 17/23] Fix import path --- znvis/mesh/box.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/znvis/mesh/box.py b/znvis/mesh/box.py index e87bc31..dc8a252 100644 --- a/znvis/mesh/box.py +++ b/znvis/mesh/box.py @@ -31,7 +31,7 @@ from znvis.transformations.rotation_matrices import rotation_matrix -from .mesh import Mesh +from znvis.mesh import Mesh @dataclass @@ -54,7 +54,7 @@ class Box(Mesh): depth: float = 1.0 def create_mesh(self) -> o3d.geometry.TriangleMesh: - + return o3d.geometry.TriangleMesh.create_box( width=self.width, height=self.height, From d6adfa6b2124929072eaeb8d499531ace6b86c28 Mon Sep 17 00:00:00 2001 From: phohenberger Date: Tue, 2 Jul 2024 22:23:18 +0200 Subject: [PATCH 18/23] Update remaining mesh --- znvis/mesh/arrow.py | 4 ++-- znvis/mesh/custom.py | 2 +- znvis/mesh/mesh.py | 2 +- znvis/particle/particle.py | 4 ++-- znvis/particle/vector_field.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/znvis/mesh/arrow.py b/znvis/mesh/arrow.py index 244b6aa..a3b5143 100644 --- a/znvis/mesh/arrow.py +++ b/znvis/mesh/arrow.py @@ -35,7 +35,7 @@ class Arrow(Mesh): """ A class to produce arrow meshes. Arrow meshes are a special case and need to - overwrite the create_mesh object of the parent mesh class. + overwrite the instantiate_mesh of the parent mesh class. Attributes ---------- @@ -47,7 +47,7 @@ class Arrow(Mesh): scale: float = 1.0 resolution: int = 10 - def create_mesh( + def instantiate_mesh( self, starting_position: np.ndarray, starting_orientation: np.ndarray = None ) -> o3d.geometry.TriangleMesh: """ diff --git a/znvis/mesh/custom.py b/znvis/mesh/custom.py index 7be97d0..826c8af 100644 --- a/znvis/mesh/custom.py +++ b/znvis/mesh/custom.py @@ -49,7 +49,7 @@ 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: """ diff --git a/znvis/mesh/mesh.py b/znvis/mesh/mesh.py index e3305dd..170110c 100644 --- a/znvis/mesh/mesh.py +++ b/znvis/mesh/mesh.py @@ -57,7 +57,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: """ diff --git a/znvis/particle/particle.py b/znvis/particle/particle.py index d75f9c1..437d5e6 100644 --- a/znvis/particle/particle.py +++ b/znvis/particle/particle.py @@ -88,9 +88,9 @@ 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: diff --git a/znvis/particle/vector_field.py b/znvis/particle/vector_field.py index 2ef1465..7b95ed6 100644 --- a/znvis/particle/vector_field.py +++ b/znvis/particle/vector_field.py @@ -81,8 +81,8 @@ def _create_mesh(self, position: np.ndarray, direction: np.ndarray): mesh : o3d.geometry.TriangleMesh 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: From 447fa7a5d21a4cb516d50cc89bf788117bc4523f Mon Sep 17 00:00:00 2001 From: phohenberger Date: Sat, 6 Jul 2024 19:38:11 +0200 Subject: [PATCH 19/23] Add dynamic mesh coloring --- znvis/mesh/mesh.py | 4 +++- znvis/particle/particle.py | 20 ++++++++++++-------- znvis/particle/vector_field.py | 15 +++++++++------ 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/znvis/mesh/mesh.py b/znvis/mesh/mesh.py index 170110c..0184ebd 100644 --- a/znvis/mesh/mesh.py +++ b/znvis/mesh/mesh.py @@ -42,13 +42,15 @@ class Mesh: material: Material = field(default_factory=lambda: Material()) base_direction: np.ndarray = field(default_factory=lambda: np.array([1, 0, 0])) + dynamic_color: bool = False def __post_init__(self): """ Post init function to create materials. """ material = rendering.MaterialRecord() - material.base_color = np.hstack((self.material.colour, self.material.alpha)) + if not self.dynamic_color: + 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 diff --git a/znvis/particle/particle.py b/znvis/particle/particle.py index 437d5e6..f4cde2b 100644 --- a/znvis/particle/particle.py +++ b/znvis/particle/particle.py @@ -71,7 +71,7 @@ class Particle: 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. @@ -91,10 +91,14 @@ def _create_mesh(self, position, director): mesh = self.mesh.instantiate_mesh(position, starting_orientation=director) else: 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.dynamic_color: + mesh.paint_uniform_color(self.mesh.material.colour[time_step, index, :]) + + return mesh def construct_mesh_list(self): """ @@ -127,16 +131,16 @@ def construct_mesh_list(self): 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 7b95ed6..94c5499 100644 --- a/znvis/particle/vector_field.py +++ b/znvis/particle/vector_field.py @@ -65,7 +65,7 @@ class VectorField: 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. @@ -84,9 +84,12 @@ def _create_mesh(self, position: np.ndarray, direction: np.ndarray): 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.dynamic_color: + mesh = mesh.paint_uniform_color(self.mesh.material.colour[time_step, index, :]) + + return mesh def construct_mesh_list(self): """ @@ -119,9 +122,9 @@ def construct_mesh_list(self): for j in range(n_particles): if np.max(np.abs(self.direction[i][j])) > 0: # ignore vectors with length zero if new_mesh is False: - mesh += self._create_mesh(self.position[i][j], self.direction[i][j]) + mesh += self._create_mesh(self.position[i][j], self.direction[i][j], i, j) else: - mesh = self._create_mesh(self.position[i][j], self.direction[i][j]) + mesh = self._create_mesh(self.position[i][j], self.direction[i][j], i, j) new_mesh = False new_mesh = True From d64daca77fa45c67d103c334533c6fc6044b56f1 Mon Sep 17 00:00:00 2001 From: Paul Hohenberger <49005843+phohenberger@users.noreply.github.com> Date: Sun, 7 Jul 2024 22:26:35 +0200 Subject: [PATCH 20/23] Unify function names --- znvis/mesh/arrow.py | 2 +- znvis/mesh/mesh.py | 2 +- znvis/mesh/sphere.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/znvis/mesh/arrow.py b/znvis/mesh/arrow.py index a3b5143..3a0d9b8 100644 --- a/znvis/mesh/arrow.py +++ b/znvis/mesh/arrow.py @@ -64,7 +64,7 @@ def instantiate_mesh( return mesh - def create_mesh_object(self, direction: np.ndarray) -> o3d.geometry.TriangleMesh: + def create_mesh(self, direction: np.ndarray) -> o3d.geometry.TriangleMesh: """ Creates an arrow mesh object. """ diff --git a/znvis/mesh/mesh.py b/znvis/mesh/mesh.py index 170110c..9d8f9a0 100644 --- a/znvis/mesh/mesh.py +++ b/znvis/mesh/mesh.py @@ -83,7 +83,7 @@ def instantiate_mesh( return mesh - def create_mesh_object(self) -> o3d.geometry.TriangleMesh: + def create_mesh(self) -> o3d.geometry.TriangleMesh: """ Create a mesh object defined by the dataclass. """ diff --git a/znvis/mesh/sphere.py b/znvis/mesh/sphere.py index 67a829d..a1fda80 100644 --- a/znvis/mesh/sphere.py +++ b/znvis/mesh/sphere.py @@ -47,7 +47,7 @@ class Sphere(Mesh): radius: float = 1.0 resolution: int = 10 - def create_mesh_object(self) -> o3d.geometry.TriangleMesh: + def create_mesh(self) -> o3d.geometry.TriangleMesh: return o3d.geometry.TriangleMesh.create_sphere( radius=self.radius, resolution=self.resolution From ab5f5e5e0b3c364aa105ce6b219c018717a0656d Mon Sep 17 00:00:00 2001 From: phohenberger Date: Wed, 10 Jul 2024 11:10:58 +0200 Subject: [PATCH 21/23] Update tests and fix function naming --- CI/unit_tests/mesh/test_cylinder.py | 2 +- CI/unit_tests/mesh/test_sphere.py | 2 +- znvis/mesh/arrow.py | 2 +- znvis/mesh/mesh.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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/znvis/mesh/arrow.py b/znvis/mesh/arrow.py index 3a0d9b8..90162cf 100644 --- a/znvis/mesh/arrow.py +++ b/znvis/mesh/arrow.py @@ -53,7 +53,7 @@ def instantiate_mesh( """ Create and correctly orient an arrow mesh. Overwrites the parent class """ - mesh = self.create_mesh_object(starting_orientation) + 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) diff --git a/znvis/mesh/mesh.py b/znvis/mesh/mesh.py index 9d992b1..67af0f0 100644 --- a/znvis/mesh/mesh.py +++ b/znvis/mesh/mesh.py @@ -76,7 +76,7 @@ def instantiate_mesh( ------- mesh : o3d.geometry.TriangleMesh """ - mesh = self.create_mesh_object() + mesh = self.create_mesh() mesh.compute_vertex_normals() mesh.translate(starting_position.astype(float)) if starting_orientation is not None: From d1e63e9936bef1c2f10401c96243128258d882b3 Mon Sep 17 00:00:00 2001 From: phohenberger Date: Wed, 10 Jul 2024 12:06:25 +0200 Subject: [PATCH 22/23] Add examples for vector_field, dynamic_color and new shapes --- examples/all_shapes.py | 123 ++++++++++++++++++++++++++++++++ examples/dynamic_coloring.py | 67 +++++++++++++++++ examples/simple_vector_field.py | 72 +++++++++++++++++++ 3 files changed, 262 insertions(+) create mode 100644 examples/all_shapes.py create mode 100644 examples/dynamic_coloring.py create mode 100644 examples/simple_vector_field.py 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..a4a2012 --- /dev/null +++ b/examples/dynamic_coloring.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 +------- +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, + dynamic_color=True) + 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() \ No newline at end of file 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 From fb840e9cf09c9349caa16e5f56606c7bd08fd4d0 Mon Sep 17 00:00:00 2001 From: phohenberger Date: Thu, 11 Jul 2024 11:16:32 +0200 Subject: [PATCH 23/23] Remove dynamic_color flag --- examples/dynamic_coloring.py | 27 +++++++++++------------ znvis/mesh/mesh.py | 7 +++--- znvis/particle/particle.py | 12 +++++------ znvis/particle/vector_field.py | 39 ++++++++++++++++++++++------------ 4 files changed, 47 insertions(+), 38 deletions(-) diff --git a/examples/dynamic_coloring.py b/examples/dynamic_coloring.py index a4a2012..3d4c3c0 100644 --- a/examples/dynamic_coloring.py +++ b/examples/dynamic_coloring.py @@ -34,34 +34,31 @@ # 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) + 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) + 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) + 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) + 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) + 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, - dynamic_color=True) + 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() \ No newline at end of file + visualizer.run_visualization() diff --git a/znvis/mesh/mesh.py b/znvis/mesh/mesh.py index 67af0f0..85cb215 100644 --- a/znvis/mesh/mesh.py +++ b/znvis/mesh/mesh.py @@ -29,6 +29,7 @@ from znvis.material.material import Material from znvis.transformations.rotation_matrices import rotation_matrix + @dataclass class Mesh: """ @@ -42,14 +43,14 @@ class Mesh: material: Material = field(default_factory=lambda: Material()) base_direction: np.ndarray = field(default_factory=lambda: np.array([1, 0, 0])) - dynamic_color: bool = False def __post_init__(self): """ Post init function to create materials. """ material = rendering.MaterialRecord() - if not self.dynamic_color: + 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 @@ -84,7 +85,7 @@ def instantiate_mesh( mesh.rotate(matrix) return mesh - + def create_mesh(self) -> o3d.geometry.TriangleMesh: """ Create a mesh object defined by the dataclass. diff --git a/znvis/particle/particle.py b/znvis/particle/particle.py index f4cde2b..b0234c2 100644 --- a/znvis/particle/particle.py +++ b/znvis/particle/particle.py @@ -53,8 +53,8 @@ class Particle: 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) + 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 @@ -91,11 +91,11 @@ def _create_mesh(self, position, director, time_step=None, index=None): mesh = self.mesh.instantiate_mesh(position, starting_orientation=director) else: mesh = self.mesh.instantiate_mesh(position) - + if self.smoothing: mesh = mesh.filter_smooth_taubin(100) - - if self.mesh.dynamic_color: + + if self.mesh.material.colour.ndim == 3: mesh.paint_uniform_color(self.mesh.material.colour[time_step, index, :]) return mesh @@ -113,7 +113,7 @@ def construct_mesh_list(self): """ self.mesh_list = [] try: - if not self.static: + if not self.static: n_particles = int(self.position.shape[1]) n_time_steps = int(self.position.shape[0]) else: diff --git a/znvis/particle/vector_field.py b/znvis/particle/vector_field.py index 94c5499..a5f746f 100644 --- a/znvis/particle/vector_field.py +++ b/znvis/particle/vector_field.py @@ -40,7 +40,7 @@ class VectorField: name : str Name of the vector field mesh : Mesh - Mesh to use + Mesh to use position : np.ndarray Position tensor of the shape (n_steps, n_vectors, n_dims) direction : np.ndarray @@ -49,8 +49,8 @@ class VectorField: 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) + 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,14 +58,16 @@ class VectorField: """ name: str - mesh: Arrow = None # Should be an instance of the Arrow class + mesh: Arrow = None # Should be an instance of the Arrow class 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, time_step: int, index: int): + def _create_mesh( + self, position: np.ndarray, direction: np.ndarray, time_step: int, index: int + ): """ Create a mesh object for the vector field. @@ -85,9 +87,11 @@ def _create_mesh(self, position: np.ndarray, direction: np.ndarray, time_step: i mesh = self.mesh.instantiate_mesh(position, direction) if self.smoothing: mesh = mesh.filter_smooth_taubin(100) - - if self.mesh.dynamic_color: - mesh = mesh.paint_uniform_color(self.mesh.material.colour[time_step, index, :]) + + if self.mesh.material.colour.ndim == 3: + mesh = mesh.paint_uniform_color( + self.mesh.material.colour[time_step, index, :] + ) return mesh @@ -104,7 +108,7 @@ def construct_mesh_list(self): """ self.mesh_list = [] try: - if not self.static: + if not self.static: n_particles = int(self.position.shape[1]) n_time_steps = int(self.position.shape[0]) else: @@ -120,12 +124,19 @@ def construct_mesh_list(self): for i in track(range(n_time_steps), description=f"Building {self.name} Mesh"): for j in range(n_particles): - if np.max(np.abs(self.direction[i][j])) > 0: # ignore vectors with length zero - if new_mesh is False: - mesh += self._create_mesh(self.position[i][j], self.direction[i][j], i, j) - else: - mesh = self._create_mesh(self.position[i][j], self.direction[i][j], 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)