diff --git a/AUVSim/auv.py b/AUVSim/auv.py index 76819f7..d1c3e3a 100644 --- a/AUVSim/auv.py +++ b/AUVSim/auv.py @@ -1,23 +1,26 @@ -from Raytrace.TriangleMesh import TriangleMesh, Ray +from Raytrace import TriangleMesh from Raytrace.SideScan import SideScan import numpy as np from typing import Any +from CustomTypes import Ray, Vector3, Vector3Array + class AUV: ideal_distance:float # Currently unused - start_pos:np.ndarray - start_facing:np.ndarray - def __init__(self, position:np.ndarray, facing:np.ndarray): + start_pos:Vector3 + start_facing:Vector3 + def __init__(self, position:Vector3, facing:Vector3): self.start_pos = position self.start_facing = facing class AUVPath: - positions:np.ndarray[float, Any] - facings:np.ndarray[float, Any] + positions:Vector3Array + facings:Vector3Array - def __init__(self, positions:np.ndarray, facings:np.ndarray): + def __init__(self, positions:Vector3Array, facings:Vector3Array): self.positions = positions self.facings = facings - def as_rays(self): - return [Ray(i, j) for i, j in zip(self.facings, self.positions)] + @property + def rays(self) -> list[Ray]: + return list(map(Ray, self.facings, self.positions)) class AUVPathGen: mesh:TriangleMesh @@ -50,10 +53,8 @@ def get_path(self, travel_distance:float, samples:int) -> AUVPath: # point in the direction that the hull is sloping rise = (ray_dist - prev_dist) * side_ray.direction if not (np.isfinite(ray_dist) and np.isfinite(prev_dist)): - rise = np.zeros(3) + rise = Vector3(0, 0, 0) new_facing = normalize(facings[-1] + rise) facings.append(new_facing) - np_positions = np.array(positions) - np_facings = np.array(facings) - return AUVPath(np_positions, np_facings) \ No newline at end of file + return AUVPath(Vector3Array(positions), Vector3Array(facings)) \ No newline at end of file diff --git a/CustomTypes/.gitignore b/CustomTypes/.gitignore new file mode 100644 index 0000000..ba0430d --- /dev/null +++ b/CustomTypes/.gitignore @@ -0,0 +1 @@ +__pycache__/ \ No newline at end of file diff --git a/CustomTypes/__init__.py b/CustomTypes/__init__.py new file mode 100644 index 0000000..ff9b0ea --- /dev/null +++ b/CustomTypes/__init__.py @@ -0,0 +1,11 @@ +from .custom_np import CustomNP as CustomNP +from .custom_types import ( + Vector3 as Vector3, + Triangle as Triangle, + Ray as Ray, +) +from .custom_arrays import ( + FloatArray as FloatArray, + TriangleArray as TriangleArray, + Vector3Array as Vector3Array, +) \ No newline at end of file diff --git a/CustomTypes/custom_arrays.py b/CustomTypes/custom_arrays.py new file mode 100644 index 0000000..e881f0b --- /dev/null +++ b/CustomTypes/custom_arrays.py @@ -0,0 +1,54 @@ +from typing import Any, Iterable, Literal, overload +import numpy as np + +from . import CustomNP, Triangle, Vector3 + +class FloatArray(CustomNP): + def __new__(cls, + content: Iterable[float]|np.ndarray|None = None, + ): + if isinstance(content, np.ndarray): + if content.ndim != 1: raise ValueError(content.shape) # must be 1d + return content.view(cls) + if content is None: + return np.ndarray((0,), float).view(cls) + return np.array(content).view(cls) + def __getitem__(self, index): + if isinstance(index, slice): return super().__getitem__(index).view(type(self)) + return super().__getitem__(index) + +class Vector3Array(CustomNP): + def __new__(cls, + content: Iterable[Vector3]|np.ndarray|None = None, + ): + if isinstance(content, np.ndarray): + if content.ndim == 1: content = content.reshape((1,) + content.shape) + if content.ndim != 2: raise ValueError(content.shape) # must be 1d or 2d + if content.shape[1] != 3: raise ValueError(content.shape) # must be shape (n, 3) + return content.view(cls) + if content is None: + return np.ndarray((0, 3), float).view(cls) + return np.array(content).view(cls) + def __getitem__(self, index): + if isinstance(index, slice): return super().__getitem__(index).view(type(self)) + return super().__getitem__(index) + +class TriangleArray(CustomNP): + def __new__(cls, + content: Iterable[Triangle]|np.ndarray|None = None, + ): + if isinstance(content, np.ndarray): + if content.ndim == 2: content = content.reshape((1,) + content.shape) + if content.ndim != 3: raise ValueError(content.shape) # must be 2d or 3d + if content.shape[1] != 3 or content.shape[2] != 3: raise ValueError(content.shape) # must be shape (n, 3, 3) + return content.view(cls) + if content is None: + return np.ndarray((0, 3, 3), float).view(cls) + return np.array(content).view(cls) + def __getitem__(self, index): + if isinstance(index, slice): return super().__getitem__(index).view(type(self)) + return super().__getitem__(index) + @property + def verticies(self) -> tuple[Vector3Array, Vector3Array, Vector3Array]: + v0, v1, v2 = self.swapaxes(0, 1) + return (v0.view(Vector3Array), v1.view(Vector3Array), v2.view(Vector3Array)) \ No newline at end of file diff --git a/CustomTypes/custom_arrays.pyi b/CustomTypes/custom_arrays.pyi new file mode 100644 index 0000000..3a20b7e --- /dev/null +++ b/CustomTypes/custom_arrays.pyi @@ -0,0 +1,51 @@ +import numpy as np +from typing import Any, Iterable, Literal, Self, Type, TypeVar, overload + +from . import CustomNP, Triangle, Vector3 + +class FloatArray(CustomNP[tuple[int, Literal[3]], np.dtype[np.float_]]): + @overload + def __new__(cls) -> FloatArray:... + @overload + def __new__(cls, content: Iterable[float]) -> FloatArray:... + @overload + def __new__(cls, content: np.ndarray[tuple[int], np.dtype[np.float_]]) -> FloatArray:... + + @overload # type: ignore + def __getitem__(self, index:int) -> float: ... + @overload + def __getitem__(self, index:slice) -> Self: ... + +class Vector3Array(CustomNP[tuple[int, Literal[3]], np.dtype[np.float_]]): + @overload + def __new__(cls) -> Vector3Array:... + @overload + def __new__(cls, content: Iterable[Vector3]) -> Vector3Array:... + @overload + def __new__(cls, content: np.ndarray[tuple[int, Literal[3]], np.dtype[np.float_]]) -> Vector3Array:... + + @overload # type: ignore + def __getitem__(self, index:int) -> Vector3: ... + @overload + def __getitem__(self, index:tuple[int, Literal[0, 1, 2]]) -> float: ... + @overload + def __getitem__(self, index:slice) -> Self: ... + +class TriangleArray(CustomNP[tuple[int, Literal[3], Literal[3]], np.dtype[np.float_]]): + @overload + def __new__(cls) -> TriangleArray:... + @overload + def __new__(cls, content: Iterable[Triangle]) -> TriangleArray:... + @overload + def __new__(cls, content: np.ndarray[tuple[int, Literal[3], Literal[3]], np.dtype[np.float_]]) -> TriangleArray:... + + @overload # type: ignore + def __getitem__(self, index:tuple[int, Literal[0, 1, 2]]) -> Vector3: ... + @overload + def __getitem__(self, index:tuple[int, Literal[0, 1, 2], Literal[0, 1, 2]]) -> float: ... + @overload + def __getitem__(self, index:slice) -> Self: ... + + @property + def verticies(self) -> tuple[Vector3Array, Vector3Array, Vector3Array]: ... + diff --git a/CustomTypes/custom_np.py b/CustomTypes/custom_np.py new file mode 100644 index 0000000..9ab0a44 --- /dev/null +++ b/CustomTypes/custom_np.py @@ -0,0 +1,21 @@ + +from typing import Generic, TypeVar, Self +import numpy as np + + +_S = TypeVar('_S', bound=tuple) +_T = TypeVar('_T', bound=np.dtype) +class CustomNP(np.ndarray[_S, _T]): + def __add__(self, other): + return super().__add__(other).view(type(self)) + def __sub__(self, other): + return super().__sub__(other).view(type(self)) + def __mul__(self, other): + return super().__mul__(other).view(type(self)) + def __truediv__(self, other): + return super().__truediv__(other).view(type(self)) + def __rtruediv__(self, other): + return super().__rtruediv__(other).view(type(self)) + @property + def nd(self): + return self.view(np.ndarray) \ No newline at end of file diff --git a/CustomTypes/custom_np.pyi b/CustomTypes/custom_np.pyi new file mode 100644 index 0000000..9786798 --- /dev/null +++ b/CustomTypes/custom_np.pyi @@ -0,0 +1,15 @@ +from typing import TypeVar, Self +import numpy as np + + +_S = TypeVar('_S', bound=tuple) +_T = TypeVar('_T', bound=np.dtype) +class CustomNP(np.ndarray[_S, _T]): + def __add__(self, other:Self) -> Self: ... # type: ignore + def __sub__(self, other:Self) -> Self: ... # type: ignore + def __mul__(self, other:Self|float) -> Self: ... # type: ignore + def __truediv__(self, other:Self|float) -> Self: ... # type: ignore + def __rtruediv__(self, other:float) -> Self: ... # type: ignore + + @property + def nd(self) -> np.ndarray: ... diff --git a/CustomTypes/custom_types.py b/CustomTypes/custom_types.py new file mode 100644 index 0000000..193a87f --- /dev/null +++ b/CustomTypes/custom_types.py @@ -0,0 +1,42 @@ +from typing import Any, Iterable, Literal, Self, overload +import numpy as np +from . import CustomNP + +class Vector3(CustomNP): + def __new__(cls, + x: float | tuple[float, float, float] | Self, y = 0, z = 0): + if isinstance(x, tuple): + x, y, z = x + elif isinstance(x, Vector3): + x, y, z = x + assert isinstance(x, (float, int)) + return np.array([x, y, z]).view(cls) + def __str__(self): + return f"({self[0]}, {self[1]}, {self[2]})" + def __repr__(self): + return f"Vector3({self[0]}, {self[1]}, {self[2]})" + +class Triangle(CustomNP): + def __new__(cls, + v0: Vector3 | tuple[Vector3, Vector3, Vector3], + v1: Vector3 = Vector3(0, 0, 0), + v2: Vector3 = Vector3(0, 0, 0), + ) -> 'Triangle': + if isinstance(v0, tuple): + v0, v1, v2 = v0 + return np.array([v0, v1, v2]).view(cls) + + def __str__(self) -> str: + return f"({self[0]}, {self[1]}, {self[2]})" + def __repr__(self) -> str: + return f"Vector3({self[0]}, {self[1]}, {self[2]})" + +class Ray: + direction:Vector3 + origin:Vector3 + def __init__(self, + direction:Vector3, + origin:Vector3, + ) -> None: + self.direction = direction + self.origin = origin \ No newline at end of file diff --git a/CustomTypes/custom_types.pyi b/CustomTypes/custom_types.pyi new file mode 100644 index 0000000..04fac9a --- /dev/null +++ b/CustomTypes/custom_types.pyi @@ -0,0 +1,40 @@ +from typing import Any, Iterable, Iterator, Literal, TypeAlias, TypeVar, overload +import numpy as np +from . import CustomNP + +class Vector3(CustomNP[tuple[Literal[3]], np.dtype[np.float_]]): + @overload + def __new__(cls, x: float, y: float, z: float) -> Vector3: ... + @overload + def __new__(cls, x: tuple[float, float, float]) -> Vector3: ... + @overload + def __new__(cls, x: Vector3) -> Vector3: ... + + def __getitem__(self, index:Literal[0, 1, 2]) -> float: ... # type: ignore + +class Triangle(CustomNP[tuple[Literal[3], Literal[3]], np.dtype[np.float_]], Iterable[Vector3]): + @overload + def __new__(cls, v0: Vector3, v1: Vector3, v2: Vector3) -> Triangle: ... + @overload + def __new__(cls, v0: tuple[Vector3, Vector3, Vector3]) -> Triangle: ... + @overload + def __new__(cls, v0: Triangle) -> Triangle: ... + + @overload # type: ignore + def __getitem__(self, index:Literal[0, 1, 2]) -> Vector3: ... + @overload + def __getitem__(self, index:tuple[Literal[0, 1, 2], Literal[0, 1, 2]]) -> float: ... + + def __add__(self, other:Triangle) -> Triangle: ... # type: ignore + def __sub__(self, other:Triangle) -> Triangle: ... # type: ignore + + def __iter__(self) -> Iterator[Vector3]: ... +class Ray: + direction:Vector3 + origin:Vector3 + def __init__(self, + direction:Vector3, + origin:Vector3, + ) -> None: + self.direction = direction + self.origin = origin \ No newline at end of file diff --git a/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb b/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb index d5f6cb9..9d5f4e9 100644 --- a/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb +++ b/Raytrace/Examples/ExampleMultipleSonarReadings.ipynb @@ -2,13 +2,13 @@ "cells": [ { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys; sys.path.append('../..')\n", - "from Raytrace.TriangleMesh import Ray\n", - "from Raytrace.BVHMesh import BVHMesh\n", + "from CustomTypes import Ray, Vector3, Vector3Array\n", + "from Raytrace import BVHMesh\n", "from Raytrace.SideScan import SideScan, ScanReading\n", "import numpy as np\n", "import time" @@ -34,18 +34,18 @@ "metadata": {}, "outputs": [], "source": [ - "facing = np.array([1, 0, 0])\n", + "facing = Vector3(1, 0, 0)\n", "min_angle = 0\n", "max_angle = -np.pi/2\n", "sample_ray_count = 1000\n", "\n", - "origin_start = np.array([5, -1, 1])\n", - "origin_end = np.array([9, -1, 1])\n", + "origin_start = Vector3(5, -1, 1)\n", + "origin_end = Vector3(9, -1, 1)\n", "readings_count = 100\n", "# liniar interpolation between the two for this simple demo\n", - "origins = [origin_start * (1-i) + origin_end * i for i in np.arange(0, 1+1/(readings_count-1)/2, 1/(readings_count-1))]\n", + "origins = Vector3Array([origin_start * (1-i) + origin_end * i for i in np.arange(0, 1+1/(readings_count-1)/2, 1/(readings_count-1))])\n", "\n", - "orientations = [Ray(facing, origin) for origin in origins]\n", + "orientations = list(map(Ray, [facing] * readings_count, origins))\n", "\n", "rays_list = [SideScan.generate_rays(orientation, min_angle, max_angle, sample_ray_count) for orientation in orientations]\n", "\n", diff --git a/Raytrace/Examples/ExampleSingleSonarReading.ipynb b/Raytrace/Examples/ExampleSingleSonarReading.ipynb index 0a89788..4c4bed6 100644 --- a/Raytrace/Examples/ExampleSingleSonarReading.ipynb +++ b/Raytrace/Examples/ExampleSingleSonarReading.ipynb @@ -2,16 +2,17 @@ "cells": [ { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import sys; sys.path.append('../..')\n", - "from Raytrace.TriangleMesh import Ray\n", - "from Raytrace.BVHMesh import BVHMesh\n", + "from CustomTypes import Ray\n", + "from Raytrace import BVHMesh\n", "from Raytrace.SideScan import SideScan, ScanReading\n", "import numpy as np\n", - "import time" + "import time\n", + "from CustomTypes import Vector3" ] }, { @@ -34,8 +35,8 @@ "metadata": {}, "outputs": [], "source": [ - "origin = np.array([5, -1, 1])\n", - "facing = np.array([1, 0, 0])\n", + "origin = Vector3(5, -1, 1)\n", + "facing = Vector3(1, 0, 0)\n", "min_angle = 0\n", "max_angle = -np.pi/2\n", "sample_ray_count = 1000\n", @@ -47,7 +48,7 @@ "raw_reading = SideScan(mesh).scan_rays(rays)\n", "reading = ScanReading(raw_reading)\n", "\n", - "print('Triangles:', mesh.triangles.shape[0])\n", + "print('Triangles:', len(mesh.triangles))\n", "raw_reading.print_summary()" ] }, diff --git a/Raytrace/Meshs/__init__.py b/Raytrace/Meshs/__init__.py new file mode 100644 index 0000000..cc039d9 --- /dev/null +++ b/Raytrace/Meshs/__init__.py @@ -0,0 +1,3 @@ +from .triangle_mesh import TriangleMesh as TriangleMesh +from .bvh_mesh import BVHMesh as BVHMesh +from .composite_mesh import CompositeMesh as CompositeMesh \ No newline at end of file diff --git a/Raytrace/BVHMesh.py b/Raytrace/Meshs/bvh_mesh.py similarity index 76% rename from Raytrace/BVHMesh.py rename to Raytrace/Meshs/bvh_mesh.py index 6edb52e..6e70524 100644 --- a/Raytrace/BVHMesh.py +++ b/Raytrace/Meshs/bvh_mesh.py @@ -1,34 +1,37 @@ -from typing import Self, Literal +from typing import Self, Literal, Type, TypeVar import numpy as np -from Raytrace.TriangleMesh import TriangleMesh, Ray + +from . import TriangleMesh +from CustomTypes import Ray, TriangleArray, Vector3, Vector3Array # Adapted from https://jacco.ompf2.com/2022/04/13/how-to-build-a-bvh-part-1-basics/ class BVHAABB: - min_pos:np.ndarray - max_pos:np.ndarray + min_pos:Vector3 + max_pos:Vector3 def __init__(self): - self.min_pos = np.array([np.nan]*3) - self.max_pos = np.array([np.nan]*3) + self.min_pos = Vector3((np.nan,)*3) + self.max_pos = Vector3((np.nan,)*3) @staticmethod - def from_array(triangles:np.ndarray) -> 'BVHAABB': + def from_array(triangles:TriangleArray) -> 'BVHAABB': out = BVHAABB() - if triangles.size == 0: return out - triangles = triangles.reshape((-1, 3)) - out.min_pos = triangles.min(axis = 0) - out.max_pos = triangles.max(axis = 0) + if len(triangles) == 0: return out + verts = triangles.reshape((-1, 3)).view(Vector3Array) + out.min_pos = verts.min(axis = 0).view(Vector3) + out.max_pos = verts.max(axis = 0).view(Vector3) + return out def raytrace(self, ray:Ray) -> float: old_error_state = np.seterr(divide='ignore') - t1 = (self.min_pos - ray.origin) * ray.inverse_direction - t2 = (self.max_pos - ray.origin) * ray.inverse_direction + t1 = (self.min_pos - ray.origin) / ray.direction + t2 = (self.max_pos - ray.origin) / ray.direction np.seterr(**old_error_state) t = np.stack([t1, t2]) tmin:float = t.min(axis=0).max() tmax:float = t.max(axis=0).min() return tmin if tmax >= tmin and tmax > 0 else np.inf - def grow(self, point:np.ndarray): - self.min_pos = np.nanmin(np.stack([point, self.min_pos]), axis=0) - self.max_pos = np.nanmax(np.stack([point, self.max_pos]), axis=0) + def grow(self, point:Vector3): + self.min_pos = np.nanmin(np.array([point, self.min_pos]), axis=0).view(Vector3) + self.max_pos = np.nanmax(np.array([point, self.max_pos]), axis=0).view(Vector3) def grow_aabb(self, other:Self): self.grow(other.min_pos) self.grow(other.max_pos) @@ -43,14 +46,15 @@ class BVHNode: right:Self start_index:int tri_count:int - def get_subarray(self, triangles:np.ndarray) -> np.ndarray: + _T = TypeVar('_T', TriangleArray, Vector3Array) + def get_subarray(self, triangles:_T) -> _T: return triangles[self.start_index:self.start_index + self.tri_count] - def update_bounds(self, triangles:np.ndarray): + def update_bounds(self, triangles:TriangleArray): self.aabb = BVHAABB.from_array(self.get_subarray(triangles)) def is_leaf(self) -> bool: return self.tri_count > 0 class BVHMesh(TriangleMesh): - centroids:np.ndarray + centroids:Vector3Array root:BVHNode node_count:int def __init__(self, *args, min_node_size = 2, **kwargs) -> None: @@ -58,12 +62,12 @@ def __init__(self, *args, min_node_size = 2, **kwargs) -> None: self.build_BVH(min_node_size) def build_BVH(self, min_node_size = 100) -> None: # calculate triangle centroids for partitioning - self.centroids = self.triangles.sum(axis = 1) / 3 + self.centroids = self.triangles.sum(axis = 1).view(Vector3Array) / 3 # assign all triangles to root node self.node_count = 1 self.root = root = BVHNode() root.start_index = 0 - root.tri_count = self.triangles.shape[0] + root.tri_count = len(self.triangles) if root.tri_count == 0: return root.update_bounds(self.triangles) # subdivide recursively @@ -80,8 +84,9 @@ def subdivide(self, node:BVHNode, min_node_size = 100) -> None: while i <= j: if self.centroids[i, axis] < split_pos: i += 1 else: - self.triangles[[i,j]] = self.triangles[[j,i]] - self.centroids[[i,j]] = self.centroids[[j,i]] + self.triangles.nd[[i,j]] = self.triangles.nd[[j,i]] + self.centroids.nd[[i,j]] = self.centroids.nd[[j,i]] + j -= 1 # abort split if one of the sides is empty left_count = i - node.start_index @@ -132,24 +137,25 @@ def BVH_raytrace(self, ray:Ray) -> float: if np.isfinite(dist2): stack.append(child2) return best - def find_best_split(self, node:BVHNode) -> tuple[float, int, float]: + def find_best_split(self, node:BVHNode) -> tuple[float, Literal[0, 1, 2], float]: BINS = 8 - axis:int = -1 + axis:Literal[0, 1, 2] = 0 split_pos:float = 0 best_cost:float = np.inf - triangles:np.ndarray[tuple[int, Literal[3], Literal[3]], np.dtype[np.float32]] = node.get_subarray(self.triangles) - centroids:np.ndarray[tuple[int, Literal[3]], np.dtype[np.float32]] = node.get_subarray(self.centroids) - for axis_i in range(3): - bounds_min = float(np.min(centroids[:,axis_i])) - bounds_max = float(np.max(centroids[:,axis_i])) + triangles:TriangleArray = node.get_subarray(self.triangles) + centroids:Vector3Array = node.get_subarray(self.centroids) + for axis_i in (0, 1, 2): + bounds_min = float(np.min(centroids.nd[:,axis_i])) + bounds_max = float(np.max(centroids.nd[:,axis_i])) if bounds_min == bounds_max: continue # populate the bins scale:float = BINS / (bounds_max - bounds_min) - bin_idx = ((centroids[:,axis_i] - bounds_min) * scale).astype(int) + bin_idx = ((centroids.nd[:,axis_i] - bounds_min) * scale).astype(int) bin_idx[bin_idx > BINS - 1] = BINS - 1 bin_bounds = [BVHAABB.from_array(triangles[bin_idx == n]) for n in range(BINS)] bin_counts = [np.count_nonzero(bin_idx == n) for n in range(BINS)] + # gather data for the 7 planes between the 8 bins left_area = np.zeros(BINS - 1, float) right_area = np.zeros(BINS - 1, float) diff --git a/Raytrace/CompositeMesh.py b/Raytrace/Meshs/composite_mesh.py similarity index 78% rename from Raytrace/CompositeMesh.py rename to Raytrace/Meshs/composite_mesh.py index f1dc5b6..3ea68a7 100644 --- a/Raytrace/CompositeMesh.py +++ b/Raytrace/Meshs/composite_mesh.py @@ -1,11 +1,12 @@ import numpy as np -from Raytrace.TriangleMesh import TriangleMesh, Ray +from . import TriangleMesh +from CustomTypes import Ray, TriangleArray class CompositeMesh(TriangleMesh): meshes: list[TriangleMesh] def __init__(self, meshes:list[TriangleMesh]) -> None: self.meshes = meshes - self.triangles = np.concatenate([mesh.triangles for mesh in self.meshes]) + self.triangles = np.concatenate([mesh.triangles for mesh in self.meshes]).view(TriangleArray) def raytrace(self, ray:Ray) -> float: distances = [mesh.raytrace(ray) for mesh in self.meshes] return min(distances, default = np.inf) \ No newline at end of file diff --git a/Raytrace/TriangleMesh.py b/Raytrace/Meshs/triangle_mesh.py similarity index 64% rename from Raytrace/TriangleMesh.py rename to Raytrace/Meshs/triangle_mesh.py index 50be840..acf3664 100644 --- a/Raytrace/TriangleMesh.py +++ b/Raytrace/Meshs/triangle_mesh.py @@ -1,27 +1,23 @@ +from typing import Any import numpy as np import pickle - -class Ray: - direction:np.ndarray - origin:np.ndarray - inverse_direction:np.ndarray - def __init__(self, direction:np.ndarray, origin:np.ndarray) -> None: - self.direction = direction - self.origin = origin - old_error_state = np.seterr(divide='ignore', invalid='ignore') - self.inverse_direction = np.ones(3) / direction - np.seterr(**old_error_state) +from CustomTypes import Triangle, TriangleArray, Ray, Vector3, Vector3Array, FloatArray +import stl class TriangleMesh: - # An n by 3 by 3 array of floats representing a list of triangles in the form triangle_index, vertex_index, axis_index - triangles:np.ndarray - - def __init__(self, triangles: np.ndarray|None = None, stl_path: str|None = None): + triangles:TriangleArray + def __init__(self, + triangles: np.ndarray|TriangleArray|None = None, + stl_path: str|None = None): if not stl_path is None: - import stl mesh = stl.Mesh.from_file(stl_path) - triangles = np.array([mesh.v0, mesh.v1, mesh.v2]).swapaxes(0, 1) - self.triangles = np.ndarray((0,3), float) if triangles is None else triangles + self.triangles = TriangleArray(np.array([mesh.v0, mesh.v1, mesh.v2]).swapaxes(0, 1)) + elif triangles is None: + self.triangles = TriangleArray([]) + elif isinstance(triangles, np.ndarray): + self.triangles = TriangleArray(triangles) + else: + self.triangles = triangles def raytrace(self, ray:Ray) -> float: return self.array_raytrace(ray) @@ -45,33 +41,33 @@ def array_raytrace(self, ray:Ray) -> float: return np.min(out) @staticmethod - def triangle_ray_intersection(triangle, ray:Ray, epsilon = 1e-10) -> float: + def triangle_ray_intersection(triangle:Triangle, ray:Ray, epsilon = 1e-10) -> float: # Translated from https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm#C++_implementation v0, v1, v2 = triangle edge1 = v1 - v0 edge2 = v2 - v0 - ray_cross_e2 = np.cross(ray.direction, edge2) - det = np.dot(edge1, ray_cross_e2) + ray_cross_e2 = np.cross(ray.direction, edge2).view(Vector3) + det:float = np.dot(edge1, ray_cross_e2) if -epsilon < det < epsilon: return -1 # This ray is parallel to this triangle. inv_det = 1.0 / det s = ray.origin - v0 - u = inv_det * np.dot(s, ray_cross_e2) + u:float = inv_det * np.dot(s, ray_cross_e2) if u < -epsilon or 1 < u: return -1 - s_cross_e1 = np.cross(s, edge1) - v = inv_det * np.dot(ray.direction, s_cross_e1) + s_cross_e1 = np.cross(s, edge1).view(Vector3) + v:float = inv_det * np.dot(ray.direction, s_cross_e1) if v < -epsilon or 1 < u + v: return -1 # At this stage we can compute t to find out where the intersection point is on the line. - t = inv_det * np.dot(edge2, s_cross_e1) + t:float = inv_det * np.dot(edge2, s_cross_e1) if t > epsilon: # ray intersection return t @@ -79,28 +75,27 @@ def triangle_ray_intersection(triangle, ray:Ray, epsilon = 1e-10) -> float: else: # This means that there is a line intersection but not a ray intersection. return -1 @staticmethod - def batch_triangle_ray_intersection(triangle_array, ray:Ray, epsilon = 1e-10) -> np.ndarray: - vecdot = lambda a, b : np.sum(a*b, axis=-1) + def batch_triangle_ray_intersection(triangle_array:TriangleArray, ray:Ray, epsilon = 1e-10) -> np.ndarray[np.floating, Any]: + def vecdot(a:Vector3Array|Vector3, b:Vector3Array) -> FloatArray: return np.sum(a*b, axis=-1).view(FloatArray) # Translated from https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm#C++_implementation - triangle_array_comp = triangle_array.swapaxes(0, 1) - v0, v1, v2 = triangle_array_comp[0], triangle_array_comp[1], triangle_array_comp[2] - + v0, v1, v2 = triangle_array.verticies + # np.subtract(v1, v0, out= v1) # np.subtract(v2, v0, out= v2) v1 = v1 - v0 v2 = v2 - v0 - ray_cross_e2 = np.cross(ray.direction, v2) + ray_cross_e2 = np.cross(ray.direction, v2).view(Vector3Array) det = vecdot(v1, ray_cross_e2) # if -epsilon < det < epsilon: # return None # This ray is parallel to this triangle. old_error_state = np.seterr(divide='ignore', invalid='ignore') inv_det = 1.0 / det - s = ray.origin - v0 + s = (ray.origin - v0).view(Vector3Array) u = inv_det * vecdot(s, ray_cross_e2) - s_cross_e1 = np.cross(s, v1) + s_cross_e1 = np.cross(s, v1).view(Vector3Array) v = inv_det * vecdot(ray.direction, s_cross_e1) valid = u < -epsilon @@ -109,7 +104,7 @@ def batch_triangle_ray_intersection(triangle_array, ray:Ray, epsilon = 1e-10) -> np.logical_or(valid, 1 < u + v, out=valid) np.invert(valid, out=valid) - t = np.empty(triangle_array.shape[0], dtype=np.float64) + t = np.empty(len(triangle_array), dtype=float) t.fill(-1) np.multiply(inv_det, vecdot(v2, s_cross_e1), out = t, where = valid) diff --git a/Raytrace/PlotRays.py b/Raytrace/PlotRays.py index 6fa0dd2..a28dccc 100644 --- a/Raytrace/PlotRays.py +++ b/Raytrace/PlotRays.py @@ -1,5 +1,5 @@ -from Raytrace.TriangleMesh import TriangleMesh -from Raytrace.SideScan import ScanReading, RawScanReading +from . import TriangleMesh +from .SideScan import ScanReading, RawScanReading import numpy as np try: import plotly.graph_objs as go # type: ignore diff --git a/Raytrace/SideScan.py b/Raytrace/SideScan.py index ac14c99..1900053 100644 --- a/Raytrace/SideScan.py +++ b/Raytrace/SideScan.py @@ -1,4 +1,5 @@ -from Raytrace.TriangleMesh import TriangleMesh, Ray +from . import TriangleMesh +from CustomTypes import Ray, Vector3, Vector3Array from typing import Self import numpy as np import time @@ -34,7 +35,7 @@ def combine_min(self, other:Self) -> Self: new_distances[other_smaller] = other.distances[other_smaller] # HACK: new rays should be generated based on which distance was closer - new_rays = [Ray(i, j) for i, j in zip(self.directions, self.origins)] + new_rays = list(map(Ray, self.directions, self.origins)) return type(self)(new_distances, new_rays) def print_summary(self) -> None: @@ -130,13 +131,14 @@ def generate_rays(orientation:Ray, min_angle:float, max_angle:float, angle_resel angle_step = (max_angle-min_angle)/(angle_reselution-1) angles = np.arange(min_angle, max_angle + angle_step/2, angle_step) origin = orientation.origin - pitch = np.arctan2(orientation.direction[2],np.sqrt(np.dot(orientation.direction[0:2], orientation.direction[0:2]))) - yaw = np.arctan2(orientation.direction[1], orientation.direction[0]) # yaw = 0 => facing due +x + pitch:float = np.arctan2(orientation.direction[2],np.sqrt(np.dot(orientation.direction.nd[0:2], orientation.direction.nd[0:2]))) + yaw:float = np.arctan2(orientation.direction[1], orientation.direction[0]) # yaw = 0 => facing due +x # Precomputed rotation matrix sin_comp = np.array([-np.sin(pitch) * np.cos(yaw), np.sin(pitch) * np.sin(yaw), np.cos(pitch)]) cos_comp = np.array([np.sin(yaw), np.cos(yaw), 0]) - return [Ray(np.sin(a) * sin_comp + np.cos(a) * cos_comp, origin) for a in angles] + directions = Vector3Array(np.array([np.sin(a) * sin_comp + np.cos(a) * cos_comp for a in angles])) + return list(map(Ray, directions, [origin]*angle_reselution)) \ No newline at end of file diff --git a/Raytrace/__init__.py b/Raytrace/__init__.py index e69de29..9954358 100644 --- a/Raytrace/__init__.py +++ b/Raytrace/__init__.py @@ -0,0 +1,5 @@ +from .Meshs import ( + TriangleMesh as TriangleMesh, + BVHMesh as BVHMesh, + CompositeMesh as CompositeMesh, +) \ No newline at end of file