Skip to content

Commit ca2e8dd

Browse files
committed
feat(pyraydeon): add numpy support
1 parent d4a3230 commit ca2e8dd

File tree

10 files changed

+405
-42
lines changed

10 files changed

+405
-42
lines changed

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ env_logger = "0.11"
1616
euclid = "0.22"
1717
float-cmp = "0.5"
1818
log = "0.4"
19-
pyo3 = "0.23"
19+
pyo3 = "0.22"
2020
rayon = "1.2"
21+
numpy = "0.22"
2122
svg = "0.18"
2223
tracing = "0.1"

pyraydeon/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ crate-type = ["cdylib"]
1111
[dependencies]
1212
pyo3 = { workspace = true, features = ["extension-module"] }
1313
raydeon.workspace = true
14+
numpy.workspace = true

pyraydeon/examples/py_cubes.py

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
"""Demonstrates custom object and numpy support
2+
3+
This example is really slow, and a good case for finding more ways to express
4+
geometry as the sum of native parts.
5+
"""
6+
7+
import numpy as np
8+
import svg
9+
10+
from pyraydeon import AABB3, Camera, Geometry, HitData, LineSegment3D, Scene
11+
12+
13+
class RectPrism(Geometry):
14+
def __init__(
15+
self,
16+
origin: np.ndarray,
17+
right: np.ndarray,
18+
width: float,
19+
up: np.ndarray,
20+
height: float,
21+
depth: float,
22+
):
23+
up = up / np.linalg.norm(up)
24+
right = right / np.linalg.norm(right)
25+
fwd = np.cross(up, right)
26+
fwd = fwd / np.linalg.norm(fwd)
27+
28+
self.origin = origin
29+
self.right = right
30+
self.up = up
31+
self.fwd = fwd
32+
33+
self.width = width
34+
self.height = height
35+
self.depth = depth
36+
37+
self.vertices = np.array(
38+
[
39+
origin,
40+
origin + right * width,
41+
origin + right * width + fwd * depth,
42+
origin + fwd * depth,
43+
origin + up * height,
44+
origin + right * width + up * height,
45+
origin + right * width + fwd * depth + up * height,
46+
origin + fwd * depth + up * height,
47+
]
48+
)
49+
50+
# Make edges stand out slightly so as to not be intersected by their own faces
51+
origin = origin - (right * 0.0015) - (up * 0.0015) - (fwd * 0.0015)
52+
width = width + 0.003
53+
height = height + 0.003
54+
depth = depth + 0.003
55+
self.path_vertices = np.array(
56+
[
57+
origin,
58+
origin + right * width,
59+
origin + right * width + fwd * depth,
60+
origin + fwd * depth,
61+
origin + up * height,
62+
origin + right * width + up * height,
63+
origin + right * width + fwd * depth + up * height,
64+
origin + fwd * depth + up * height,
65+
]
66+
)
67+
68+
self.faces = [
69+
[0, 1, 2, 3],
70+
[4, 5, 6, 7],
71+
[0, 1, 5, 4],
72+
[1, 2, 6, 5],
73+
[2, 3, 7, 6],
74+
[3, 0, 4, 7],
75+
]
76+
self.planes = [self.compute_plane(self.vertices[face]) for face in self.faces]
77+
78+
def __repr__(self):
79+
return f"RectPrism(basis='[{self.right}, {self.up}, {self.fwd}]', dims='[{self.width}, {self.height}, {self.depth}]')"
80+
81+
def compute_plane(self, points):
82+
p1, p2, p3 = points[:3]
83+
normal = np.cross(p2 - p1, p3 - p1)
84+
normal /= np.linalg.norm(normal)
85+
d = -np.dot(normal, p1)
86+
return normal, d
87+
88+
def ray_intersects_plane(self, ray, plane) -> HitData | None:
89+
normal, d = plane
90+
91+
denom = np.dot(normal, ray.dir)
92+
if abs(denom) < 1e-6:
93+
return None
94+
t = -(np.dot(normal, ray.point) + d) / denom
95+
return HitData(ray.point + t * ray.dir, t) if t >= 0 else None
96+
97+
def is_point_in_face(self, point, face):
98+
face_vertices = self.vertices[face]
99+
edge1 = face_vertices[1] - face_vertices[0]
100+
edge2 = face_vertices[3] - face_vertices[0]
101+
v = point - face_vertices[0]
102+
u1 = np.dot(v, edge1) / np.dot(edge1, edge1)
103+
u2 = np.dot(v, edge2) / np.dot(edge2, edge2)
104+
return 0 <= u1 <= 1 and 0 <= u2 <= 1
105+
106+
def hit_by(self, ray) -> HitData | None:
107+
if not self.bounding_box().hit_by(ray):
108+
return None
109+
for face, plane in zip(self.faces, self.planes):
110+
intersection = self.ray_intersects_plane(ray, plane)
111+
if intersection is not None and self.is_point_in_face(
112+
intersection.hit_point, face
113+
):
114+
return intersection
115+
116+
def bounding_box(self):
117+
my_min = np.minimum.reduce(self.vertices)
118+
my_max = np.maximum.reduce(self.vertices)
119+
return AABB3(my_min, my_max)
120+
121+
def paths(self, cam):
122+
edges = set(
123+
[
124+
tuple(sorted((face[i], face[(i + 1) % len(face)])))
125+
for face in self.faces
126+
for i in range(len(face))
127+
]
128+
)
129+
paths = [
130+
LineSegment3D(self.path_vertices[edge[0]], self.path_vertices[edge[1]])
131+
for edge in edges
132+
]
133+
return paths
134+
135+
136+
up = np.array([-1.0, 1.0, 0.0])
137+
up = up / np.linalg.norm(up)
138+
right = np.array([1.0, 1.0, 0.0])
139+
right = right / np.linalg.norm(right)
140+
141+
142+
r = RectPrism(
143+
origin=np.array([0.0, 0.0, 0.0]),
144+
right=right,
145+
width=1.0,
146+
up=up,
147+
height=1.0,
148+
depth=1.0,
149+
)
150+
151+
scene = Scene(
152+
[
153+
RectPrism(
154+
origin=np.array([0.0, 0.0, 0.0]),
155+
right=right,
156+
width=1.0,
157+
up=up,
158+
height=1.0,
159+
depth=1.0,
160+
),
161+
RectPrism(
162+
origin=np.array([0.0, 0.0, 1.25]),
163+
right=right,
164+
width=1.0,
165+
up=up,
166+
height=1.0,
167+
depth=1.0,
168+
),
169+
RectPrism(
170+
origin=right * 1.1,
171+
right=right,
172+
width=1.0,
173+
up=up,
174+
height=1.0,
175+
depth=1.0,
176+
),
177+
]
178+
)
179+
180+
eye = np.array([0.25, 3, 6])
181+
focus = np.array([0, 0, 0])
182+
up = np.array([0, 1, 0])
183+
184+
fovy = 60.0
185+
width = 1024
186+
height = 1024
187+
znear = 0.1
188+
zfar = 10.0
189+
190+
cam = Camera.look_at(eye, focus, up).perspective(fovy, width, height, znear, zfar)
191+
192+
paths = scene.render(cam)
193+
194+
canvas = svg.SVG(
195+
width="8in",
196+
height="8in",
197+
viewBox="0 0 1024 1024",
198+
)
199+
backing_rect = svg.Rect(
200+
x=0,
201+
y=0,
202+
width="100%",
203+
height="100%",
204+
fill="white",
205+
)
206+
svg_lines = [
207+
svg.Line(
208+
x1=f"{path.p1[0]}",
209+
y1=f"{path.p1[1]}",
210+
x2=f"{path.p2[0]}",
211+
y2=f"{path.p2[1]}",
212+
stroke_width="0.7mm",
213+
stroke="black",
214+
)
215+
for path in paths
216+
]
217+
line_group = svg.G(transform=f"translate(0, {height}) scale(1, -1)", elements=svg_lines)
218+
canvas.elements = [backing_rect, line_group]
219+
220+
221+
print(canvas)
Lines changed: 1 addition & 0 deletions
Loading

pyraydeon/examples/triangles.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pyraydeon import Camera, Point3, Scene, Tri, Vec3, Geometry
44

55

6-
class CustomObject(Geometry):
6+
class CustomTriangle(Geometry):
77
def __init__(self, p1, p2, p3):
88
self.tri = Tri(p1, p2, p3)
99

@@ -19,7 +19,7 @@ def bounding_box(self):
1919

2020
scene = Scene(
2121
[
22-
CustomObject(
22+
CustomTriangle(
2323
Point3(0, 0, 0),
2424
Point3(0, 0, 1),
2525
Point3(1, 0, 1),
@@ -60,10 +60,10 @@ def bounding_box(self):
6060
)
6161
svg_lines = [
6262
svg.Line(
63-
x1=f"{path.p1.x}",
64-
y1=f"{path.p1.y}",
65-
x2=f"{path.p2.x}",
66-
y2=f"{path.p2.y}",
63+
x1=f"{path.p1[0]}",
64+
y1=f"{path.p1[1]}",
65+
x2=f"{path.p2[0]}",
66+
y2=f"{path.p2[1]}",
6767
stroke_width="0.7mm",
6868
stroke="black",
6969
)

pyraydeon/pyproject.toml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ build-backend = "maturin"
44

55
[project]
66
name = "pyraydeon"
7-
version = "0.1.0-alpha"
7+
version = "0.1.0-alpha3"
88
requires-python = ">=3.9"
9-
dependencies = []
9+
dependencies = [
10+
"numpy",
11+
]
1012
classifiers = [
1113
"Programming Language :: Rust",
1214
"Programming Language :: Python :: Implementation :: CPython",

0 commit comments

Comments
 (0)