raystrack 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- raystrack/__init__.py +30 -0
- raystrack/api.py +198 -0
- raystrack/io.py +238 -0
- raystrack/main.py +2194 -0
- raystrack/params.py +129 -0
- raystrack/utils/__init__.py +23 -0
- raystrack/utils/bvh.py +74 -0
- raystrack/utils/cpu_trace.py +811 -0
- raystrack/utils/cuda_trace.py +1285 -0
- raystrack/utils/geometry.py +66 -0
- raystrack/utils/halton.py +61 -0
- raystrack/utils/helpers.py +284 -0
- raystrack/utils/prepared.py +442 -0
- raystrack/utils/ray_builder.py +97 -0
- raystrack-1.0.1.dist-info/METADATA +125 -0
- raystrack-1.0.1.dist-info/RECORD +19 -0
- raystrack-1.0.1.dist-info/WHEEL +5 -0
- raystrack-1.0.1.dist-info/licenses/LICENSE +674 -0
- raystrack-1.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .bvh import build_bvh
|
|
9
|
+
from .halton import cached_halton, cached_halton_dims
|
|
10
|
+
from .helpers import grid_from_density
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class PreparedScene:
|
|
15
|
+
v0: np.ndarray
|
|
16
|
+
e1: np.ndarray
|
|
17
|
+
e2: np.ndarray
|
|
18
|
+
normals: np.ndarray
|
|
19
|
+
sid: np.ndarray
|
|
20
|
+
bb_min: Optional[np.ndarray]
|
|
21
|
+
bb_max: Optional[np.ndarray]
|
|
22
|
+
left: Optional[np.ndarray]
|
|
23
|
+
right: Optional[np.ndarray]
|
|
24
|
+
start: Optional[np.ndarray]
|
|
25
|
+
count: Optional[np.ndarray]
|
|
26
|
+
use_bvh: bool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class PreparedEmitter:
|
|
31
|
+
tri_a: np.ndarray
|
|
32
|
+
tri_e1: np.ndarray
|
|
33
|
+
tri_e2: np.ndarray
|
|
34
|
+
tri_u: np.ndarray
|
|
35
|
+
tri_v: np.ndarray
|
|
36
|
+
tri_n: np.ndarray
|
|
37
|
+
tri_origin_eps: np.ndarray
|
|
38
|
+
plane_origin: np.ndarray
|
|
39
|
+
plane_normal: np.ndarray
|
|
40
|
+
plane_tol: float
|
|
41
|
+
plane_is_planar: bool
|
|
42
|
+
cdf: np.ndarray
|
|
43
|
+
total_area: float
|
|
44
|
+
g: int
|
|
45
|
+
u_grid: np.ndarray
|
|
46
|
+
v_grid: np.ndarray
|
|
47
|
+
halton_tri: np.ndarray
|
|
48
|
+
halton_u: np.ndarray
|
|
49
|
+
halton_v: np.ndarray
|
|
50
|
+
halton_r1: np.ndarray
|
|
51
|
+
halton_r2: np.ndarray
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def n_cells(self) -> int:
|
|
55
|
+
return int(self.u_grid.shape[0])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class PreparedDeviceScene:
|
|
60
|
+
v0: Any
|
|
61
|
+
e1: Any
|
|
62
|
+
e2: Any
|
|
63
|
+
normals: Any
|
|
64
|
+
sid: Any
|
|
65
|
+
bb_min: Optional[Any]
|
|
66
|
+
bb_max: Optional[Any]
|
|
67
|
+
left: Optional[Any]
|
|
68
|
+
right: Optional[Any]
|
|
69
|
+
start: Optional[Any]
|
|
70
|
+
count: Optional[Any]
|
|
71
|
+
use_bvh: bool
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class PreparedDeviceEmitter:
|
|
76
|
+
u_grid: Any
|
|
77
|
+
v_grid: Any
|
|
78
|
+
halton_tri: Any
|
|
79
|
+
halton_u: Any
|
|
80
|
+
halton_v: Any
|
|
81
|
+
halton_r1: Any
|
|
82
|
+
halton_r2: Any
|
|
83
|
+
cdf: Any
|
|
84
|
+
tri_a: Any
|
|
85
|
+
tri_e1: Any
|
|
86
|
+
tri_e2: Any
|
|
87
|
+
tri_u: Any
|
|
88
|
+
tri_v: Any
|
|
89
|
+
tri_n: Any
|
|
90
|
+
tri_origin_eps: Any
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _safe_normalize(v: np.ndarray) -> np.ndarray:
|
|
94
|
+
n = np.linalg.norm(v, axis=1, keepdims=True)
|
|
95
|
+
n = np.maximum(n, 1e-12)
|
|
96
|
+
return v / n
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _triangle_frames(tri_n: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
100
|
+
tri_u = np.empty_like(tri_n, dtype=np.float32)
|
|
101
|
+
tri_v = np.empty_like(tri_n, dtype=np.float32)
|
|
102
|
+
|
|
103
|
+
axis_x = np.asarray([1.0, 0.0, 0.0], dtype=np.float32)
|
|
104
|
+
axis_y = np.asarray([0.0, 1.0, 0.0], dtype=np.float32)
|
|
105
|
+
|
|
106
|
+
for i in range(tri_n.shape[0]):
|
|
107
|
+
n = tri_n[i]
|
|
108
|
+
ref = axis_x if abs(float(n[0])) < 0.9 else axis_y
|
|
109
|
+
u = np.cross(ref, n).astype(np.float32)
|
|
110
|
+
u_len = float(np.linalg.norm(u))
|
|
111
|
+
if u_len <= 1e-12:
|
|
112
|
+
ref = axis_y if ref is axis_x else axis_x
|
|
113
|
+
u = np.cross(ref, n).astype(np.float32)
|
|
114
|
+
u_len = float(np.linalg.norm(u))
|
|
115
|
+
if u_len <= 1e-12:
|
|
116
|
+
tri_u[i] = axis_x
|
|
117
|
+
tri_v[i] = axis_y
|
|
118
|
+
continue
|
|
119
|
+
u /= u_len
|
|
120
|
+
tri_u[i] = u
|
|
121
|
+
tri_v[i] = np.cross(n, u).astype(np.float32)
|
|
122
|
+
return tri_u, tri_v
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _triangle_origin_eps(tri_e1: np.ndarray, tri_e2: np.ndarray) -> np.ndarray:
|
|
126
|
+
edge_a = np.linalg.norm(tri_e1, axis=1)
|
|
127
|
+
edge_b = np.linalg.norm(tri_e2, axis=1)
|
|
128
|
+
edge_c = np.linalg.norm(tri_e2 - tri_e1, axis=1)
|
|
129
|
+
scale = np.maximum(edge_a, np.maximum(edge_b, edge_c))
|
|
130
|
+
return np.maximum(scale * 1.0e-6, 1.0e-8).astype(np.float32, copy=False)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _emitter_plane(
|
|
134
|
+
tri_a: np.ndarray,
|
|
135
|
+
tri_e1: np.ndarray,
|
|
136
|
+
tri_e2: np.ndarray,
|
|
137
|
+
tri_n: np.ndarray,
|
|
138
|
+
tri_origin_eps: np.ndarray,
|
|
139
|
+
) -> Tuple[np.ndarray, np.ndarray, float, bool]:
|
|
140
|
+
plane_origin = np.zeros(3, dtype=np.float32)
|
|
141
|
+
plane_normal = np.zeros(3, dtype=np.float32)
|
|
142
|
+
plane_tol = float(max(1.0e-7, np.max(tri_origin_eps) if tri_origin_eps.size else 0.0))
|
|
143
|
+
|
|
144
|
+
if tri_a.shape[0] == 0:
|
|
145
|
+
return plane_origin, plane_normal, plane_tol, False
|
|
146
|
+
|
|
147
|
+
plane_origin = np.asarray(tri_a[0], dtype=np.float32)
|
|
148
|
+
plane_normal = np.asarray(tri_n[0], dtype=np.float32)
|
|
149
|
+
normal_len = float(np.linalg.norm(plane_normal))
|
|
150
|
+
if normal_len <= 1.0e-12:
|
|
151
|
+
return plane_origin, plane_normal, plane_tol, False
|
|
152
|
+
|
|
153
|
+
plane_normal = (plane_normal / normal_len).astype(np.float32, copy=False)
|
|
154
|
+
normal_align = tri_n @ plane_normal
|
|
155
|
+
if np.any(normal_align < (1.0 - 1.0e-4)):
|
|
156
|
+
return plane_origin, plane_normal, plane_tol, False
|
|
157
|
+
|
|
158
|
+
offsets = (
|
|
159
|
+
np.abs((tri_a - plane_origin) @ plane_normal),
|
|
160
|
+
np.abs((tri_a + tri_e1 - plane_origin) @ plane_normal),
|
|
161
|
+
np.abs((tri_a + tri_e2 - plane_origin) @ plane_normal),
|
|
162
|
+
)
|
|
163
|
+
max_dev = max(float(np.max(arr)) if arr.size else 0.0 for arr in offsets)
|
|
164
|
+
if max_dev > plane_tol:
|
|
165
|
+
return plane_origin, plane_normal, plane_tol, False
|
|
166
|
+
|
|
167
|
+
return plane_origin, plane_normal, plane_tol, True
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def prepare_scene(
|
|
171
|
+
meshes: List[Tuple[str, np.ndarray, np.ndarray]],
|
|
172
|
+
*,
|
|
173
|
+
use_bvh: bool,
|
|
174
|
+
) -> PreparedScene:
|
|
175
|
+
v0s = []
|
|
176
|
+
e1s = []
|
|
177
|
+
e2s = []
|
|
178
|
+
normals = []
|
|
179
|
+
sids = []
|
|
180
|
+
|
|
181
|
+
for sid, (_, V, F) in enumerate(meshes):
|
|
182
|
+
tri_a = np.asarray(V[F[:, 0]], dtype=np.float32)
|
|
183
|
+
tri_b = np.asarray(V[F[:, 1]], dtype=np.float32)
|
|
184
|
+
tri_c = np.asarray(V[F[:, 2]], dtype=np.float32)
|
|
185
|
+
|
|
186
|
+
tri_e1 = (tri_b - tri_a).astype(np.float32, copy=False)
|
|
187
|
+
tri_e2 = (tri_c - tri_a).astype(np.float32, copy=False)
|
|
188
|
+
tri_n = np.cross(tri_e1, tri_e2).astype(np.float32, copy=False)
|
|
189
|
+
tri_n = _safe_normalize(tri_n).astype(np.float32, copy=False)
|
|
190
|
+
|
|
191
|
+
v0s.append(tri_a)
|
|
192
|
+
e1s.append(tri_e1)
|
|
193
|
+
e2s.append(tri_e2)
|
|
194
|
+
normals.append(tri_n)
|
|
195
|
+
sids.append(np.full(F.shape[0], sid, dtype=np.int32))
|
|
196
|
+
|
|
197
|
+
if not v0s:
|
|
198
|
+
empty3 = np.empty((0, 3), dtype=np.float32)
|
|
199
|
+
empty1 = np.empty((0,), dtype=np.int32)
|
|
200
|
+
return PreparedScene(
|
|
201
|
+
empty3,
|
|
202
|
+
empty3,
|
|
203
|
+
empty3,
|
|
204
|
+
empty3,
|
|
205
|
+
empty1,
|
|
206
|
+
None,
|
|
207
|
+
None,
|
|
208
|
+
None,
|
|
209
|
+
None,
|
|
210
|
+
None,
|
|
211
|
+
None,
|
|
212
|
+
False,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
v0 = np.concatenate(v0s, axis=0)
|
|
216
|
+
e1 = np.concatenate(e1s, axis=0)
|
|
217
|
+
e2 = np.concatenate(e2s, axis=0)
|
|
218
|
+
tri_n = np.concatenate(normals, axis=0)
|
|
219
|
+
sid = np.concatenate(sids, axis=0)
|
|
220
|
+
|
|
221
|
+
bb_min = bb_max = left = right = start = count = None
|
|
222
|
+
if use_bvh and v0.shape[0] > 0:
|
|
223
|
+
bb_min, bb_max, left, right, start, count, perm = build_bvh(v0, e1, e2)
|
|
224
|
+
v0 = v0[perm]
|
|
225
|
+
e1 = e1[perm]
|
|
226
|
+
e2 = e2[perm]
|
|
227
|
+
tri_n = tri_n[perm]
|
|
228
|
+
sid = sid[perm]
|
|
229
|
+
|
|
230
|
+
return PreparedScene(
|
|
231
|
+
v0=v0,
|
|
232
|
+
e1=e1,
|
|
233
|
+
e2=e2,
|
|
234
|
+
normals=tri_n,
|
|
235
|
+
sid=sid,
|
|
236
|
+
bb_min=bb_min,
|
|
237
|
+
bb_max=bb_max,
|
|
238
|
+
left=left,
|
|
239
|
+
right=right,
|
|
240
|
+
start=start,
|
|
241
|
+
count=count,
|
|
242
|
+
use_bvh=bool(use_bvh and v0.shape[0] > 0),
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def prepare_emitters(
|
|
247
|
+
meshes: List[Tuple[str, np.ndarray, np.ndarray]],
|
|
248
|
+
*,
|
|
249
|
+
samples: int,
|
|
250
|
+
rays: int,
|
|
251
|
+
flip_faces: bool,
|
|
252
|
+
) -> List[PreparedEmitter]:
|
|
253
|
+
emitters: List[PreparedEmitter] = []
|
|
254
|
+
for _, V, F in meshes:
|
|
255
|
+
F_emit = F[:, [0, 2, 1]] if flip_faces else F
|
|
256
|
+
tri_a = np.asarray(V[F_emit[:, 0]], dtype=np.float32)
|
|
257
|
+
tri_b = np.asarray(V[F_emit[:, 1]], dtype=np.float32)
|
|
258
|
+
tri_c = np.asarray(V[F_emit[:, 2]], dtype=np.float32)
|
|
259
|
+
|
|
260
|
+
tri_e1 = (tri_b - tri_a).astype(np.float32, copy=False)
|
|
261
|
+
tri_e2 = (tri_c - tri_a).astype(np.float32, copy=False)
|
|
262
|
+
|
|
263
|
+
tri_n_raw = np.cross(tri_e1, tri_e2).astype(np.float32, copy=False)
|
|
264
|
+
twice_area = np.linalg.norm(tri_n_raw, axis=1)
|
|
265
|
+
tri_n = _safe_normalize(tri_n_raw).astype(np.float32, copy=False)
|
|
266
|
+
tri_u, tri_v = _triangle_frames(tri_n)
|
|
267
|
+
tri_origin_eps = _triangle_origin_eps(tri_e1, tri_e2)
|
|
268
|
+
plane_origin, plane_normal, plane_tol, plane_is_planar = _emitter_plane(
|
|
269
|
+
tri_a,
|
|
270
|
+
tri_e1,
|
|
271
|
+
tri_e2,
|
|
272
|
+
tri_n,
|
|
273
|
+
tri_origin_eps,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
areas = 0.5 * twice_area
|
|
277
|
+
total_area = float(areas.sum())
|
|
278
|
+
if total_area <= 0.0:
|
|
279
|
+
cdf = np.ones(F_emit.shape[0], dtype=np.float32)
|
|
280
|
+
g = 4
|
|
281
|
+
u_grid = np.zeros(g * g, dtype=np.float32)
|
|
282
|
+
v_grid = np.zeros_like(u_grid)
|
|
283
|
+
halton_tri = np.zeros(g * g * rays, dtype=np.float32)
|
|
284
|
+
halton_u = np.zeros_like(halton_tri)
|
|
285
|
+
halton_v = np.zeros_like(halton_tri)
|
|
286
|
+
halton_r1 = np.zeros_like(halton_tri)
|
|
287
|
+
halton_r2 = np.zeros_like(halton_tri)
|
|
288
|
+
else:
|
|
289
|
+
cdf = np.cumsum(areas, dtype=np.float64)
|
|
290
|
+
cdf = (cdf / cdf[-1]).astype(np.float32)
|
|
291
|
+
g = grid_from_density(total_area, samples)
|
|
292
|
+
u_grid, v_grid = cached_halton(g)
|
|
293
|
+
n_rays_once = g * g * rays
|
|
294
|
+
halton_tri, halton_u, halton_v, halton_r1, halton_r2 = cached_halton_dims(n_rays_once)
|
|
295
|
+
|
|
296
|
+
emitters.append(
|
|
297
|
+
PreparedEmitter(
|
|
298
|
+
tri_a=tri_a,
|
|
299
|
+
tri_e1=tri_e1,
|
|
300
|
+
tri_e2=tri_e2,
|
|
301
|
+
tri_u=tri_u.astype(np.float32, copy=False),
|
|
302
|
+
tri_v=tri_v.astype(np.float32, copy=False),
|
|
303
|
+
tri_n=tri_n,
|
|
304
|
+
tri_origin_eps=tri_origin_eps,
|
|
305
|
+
plane_origin=plane_origin,
|
|
306
|
+
plane_normal=plane_normal,
|
|
307
|
+
plane_tol=plane_tol,
|
|
308
|
+
plane_is_planar=plane_is_planar,
|
|
309
|
+
cdf=cdf,
|
|
310
|
+
total_area=total_area,
|
|
311
|
+
g=g,
|
|
312
|
+
u_grid=u_grid,
|
|
313
|
+
v_grid=v_grid,
|
|
314
|
+
halton_tri=halton_tri,
|
|
315
|
+
halton_u=halton_u,
|
|
316
|
+
halton_v=halton_v,
|
|
317
|
+
halton_r1=halton_r1,
|
|
318
|
+
halton_r2=halton_r2,
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
return emitters
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class PreparedSolver:
|
|
325
|
+
"""Cache prepared geometry, ray-generation tables and CUDA uploads.
|
|
326
|
+
|
|
327
|
+
Reusing a single instance across repeated solves on the same mesh set
|
|
328
|
+
avoids rebuilding triangle buffers, BVHs, Halton tables and device copies.
|
|
329
|
+
"""
|
|
330
|
+
|
|
331
|
+
def __init__(self, meshes: List[Tuple[str, np.ndarray, np.ndarray]]):
|
|
332
|
+
self.meshes = list(meshes)
|
|
333
|
+
self.total_faces = int(sum(F.shape[0] for _, _, F in self.meshes))
|
|
334
|
+
self._scene_cache: Dict[bool, PreparedScene] = {}
|
|
335
|
+
self._emitter_cache: Dict[Tuple[int, int, bool], List[PreparedEmitter]] = {}
|
|
336
|
+
self._device_scene_cache: Dict[Tuple[int, bool], PreparedDeviceScene] = {}
|
|
337
|
+
self._device_emitter_cache: Dict[Tuple[int, int, int, int, bool], PreparedDeviceEmitter] = {}
|
|
338
|
+
self._mesh_bounds_cache: Optional[Tuple[np.ndarray, np.ndarray]] = None
|
|
339
|
+
|
|
340
|
+
def get_scene(self, *, use_bvh: bool) -> PreparedScene:
|
|
341
|
+
key = bool(use_bvh)
|
|
342
|
+
scene = self._scene_cache.get(key)
|
|
343
|
+
if scene is None:
|
|
344
|
+
scene = prepare_scene(self.meshes, use_bvh=key)
|
|
345
|
+
self._scene_cache[key] = scene
|
|
346
|
+
return scene
|
|
347
|
+
|
|
348
|
+
def get_emitters(self, *, samples: int, rays: int, flip_faces: bool) -> List[PreparedEmitter]:
|
|
349
|
+
key = (int(samples), int(rays), bool(flip_faces))
|
|
350
|
+
emitters = self._emitter_cache.get(key)
|
|
351
|
+
if emitters is None:
|
|
352
|
+
emitters = prepare_emitters(self.meshes, samples=samples, rays=rays, flip_faces=flip_faces)
|
|
353
|
+
self._emitter_cache[key] = emitters
|
|
354
|
+
return emitters
|
|
355
|
+
|
|
356
|
+
def get_emitter(self, index: int, *, samples: int, rays: int, flip_faces: bool) -> PreparedEmitter:
|
|
357
|
+
return self.get_emitters(samples=samples, rays=rays, flip_faces=flip_faces)[int(index)]
|
|
358
|
+
|
|
359
|
+
def get_mesh_bounds(self) -> Tuple[np.ndarray, np.ndarray]:
|
|
360
|
+
bounds = self._mesh_bounds_cache
|
|
361
|
+
if bounds is None:
|
|
362
|
+
n_mesh = len(self.meshes)
|
|
363
|
+
centers = np.zeros((n_mesh, 3), dtype=np.float32)
|
|
364
|
+
extents = np.zeros((n_mesh, 3), dtype=np.float32)
|
|
365
|
+
for idx, (_, V, _) in enumerate(self.meshes):
|
|
366
|
+
if V.size == 0:
|
|
367
|
+
continue
|
|
368
|
+
v = np.asarray(V, dtype=np.float32)
|
|
369
|
+
vmin = np.min(v, axis=0)
|
|
370
|
+
vmax = np.max(v, axis=0)
|
|
371
|
+
centers[idx] = 0.5 * (vmin + vmax)
|
|
372
|
+
extents[idx] = 0.5 * (vmax - vmin)
|
|
373
|
+
bounds = (centers, extents)
|
|
374
|
+
self._mesh_bounds_cache = bounds
|
|
375
|
+
return bounds
|
|
376
|
+
|
|
377
|
+
def clear_device_cache(self) -> None:
|
|
378
|
+
self._device_scene_cache.clear()
|
|
379
|
+
self._device_emitter_cache.clear()
|
|
380
|
+
|
|
381
|
+
def get_device_scene(self, *, use_bvh: bool) -> PreparedDeviceScene:
|
|
382
|
+
from numba import cuda
|
|
383
|
+
|
|
384
|
+
key = (int(cuda.get_current_device().id), bool(use_bvh))
|
|
385
|
+
scene = self._device_scene_cache.get(key)
|
|
386
|
+
if scene is None:
|
|
387
|
+
host_scene = self.get_scene(use_bvh=use_bvh)
|
|
388
|
+
scene = PreparedDeviceScene(
|
|
389
|
+
v0=cuda.to_device(host_scene.v0),
|
|
390
|
+
e1=cuda.to_device(host_scene.e1),
|
|
391
|
+
e2=cuda.to_device(host_scene.e2),
|
|
392
|
+
normals=cuda.to_device(host_scene.normals),
|
|
393
|
+
sid=cuda.to_device(host_scene.sid),
|
|
394
|
+
bb_min=None if host_scene.bb_min is None else cuda.to_device(host_scene.bb_min),
|
|
395
|
+
bb_max=None if host_scene.bb_max is None else cuda.to_device(host_scene.bb_max),
|
|
396
|
+
left=None if host_scene.left is None else cuda.to_device(host_scene.left),
|
|
397
|
+
right=None if host_scene.right is None else cuda.to_device(host_scene.right),
|
|
398
|
+
start=None if host_scene.start is None else cuda.to_device(host_scene.start),
|
|
399
|
+
count=None if host_scene.count is None else cuda.to_device(host_scene.count),
|
|
400
|
+
use_bvh=host_scene.use_bvh,
|
|
401
|
+
)
|
|
402
|
+
self._device_scene_cache[key] = scene
|
|
403
|
+
return scene
|
|
404
|
+
|
|
405
|
+
def get_device_emitter(self, index: int, *, samples: int, rays: int, flip_faces: bool) -> PreparedDeviceEmitter:
|
|
406
|
+
from numba import cuda
|
|
407
|
+
|
|
408
|
+
idx = int(index)
|
|
409
|
+
key = (int(cuda.get_current_device().id), idx, int(samples), int(rays), bool(flip_faces))
|
|
410
|
+
emitter = self._device_emitter_cache.get(key)
|
|
411
|
+
if emitter is None:
|
|
412
|
+
host_emitter = self.get_emitter(idx, samples=samples, rays=rays, flip_faces=flip_faces)
|
|
413
|
+
emitter = PreparedDeviceEmitter(
|
|
414
|
+
u_grid=cuda.to_device(host_emitter.u_grid),
|
|
415
|
+
v_grid=cuda.to_device(host_emitter.v_grid),
|
|
416
|
+
halton_tri=cuda.to_device(host_emitter.halton_tri),
|
|
417
|
+
halton_u=cuda.to_device(host_emitter.halton_u),
|
|
418
|
+
halton_v=cuda.to_device(host_emitter.halton_v),
|
|
419
|
+
halton_r1=cuda.to_device(host_emitter.halton_r1),
|
|
420
|
+
halton_r2=cuda.to_device(host_emitter.halton_r2),
|
|
421
|
+
cdf=cuda.to_device(host_emitter.cdf),
|
|
422
|
+
tri_a=cuda.to_device(host_emitter.tri_a),
|
|
423
|
+
tri_e1=cuda.to_device(host_emitter.tri_e1),
|
|
424
|
+
tri_e2=cuda.to_device(host_emitter.tri_e2),
|
|
425
|
+
tri_u=cuda.to_device(host_emitter.tri_u),
|
|
426
|
+
tri_v=cuda.to_device(host_emitter.tri_v),
|
|
427
|
+
tri_n=cuda.to_device(host_emitter.tri_n),
|
|
428
|
+
tri_origin_eps=cuda.to_device(host_emitter.tri_origin_eps),
|
|
429
|
+
)
|
|
430
|
+
self._device_emitter_cache[key] = emitter
|
|
431
|
+
return emitter
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
__all__ = [
|
|
435
|
+
"PreparedScene",
|
|
436
|
+
"PreparedEmitter",
|
|
437
|
+
"PreparedDeviceScene",
|
|
438
|
+
"PreparedDeviceEmitter",
|
|
439
|
+
"PreparedSolver",
|
|
440
|
+
"prepare_scene",
|
|
441
|
+
"prepare_emitters",
|
|
442
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
import numba as nb
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@nb.njit(inline="always", cache=True)
|
|
9
|
+
def _binary_search_cdf(cdf, x):
|
|
10
|
+
lo = 0
|
|
11
|
+
hi = cdf.shape[0] - 1
|
|
12
|
+
while lo <= hi:
|
|
13
|
+
mid = (lo + hi) // 2
|
|
14
|
+
if cdf[mid] < x:
|
|
15
|
+
lo = mid + 1
|
|
16
|
+
else:
|
|
17
|
+
hi = mid - 1
|
|
18
|
+
if lo < 0:
|
|
19
|
+
return 0
|
|
20
|
+
if lo >= cdf.shape[0]:
|
|
21
|
+
return cdf.shape[0] - 1
|
|
22
|
+
return lo
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@nb.njit(parallel=True, cache=True, fastmath=True)
|
|
26
|
+
def build_rays(
|
|
27
|
+
u_grid,
|
|
28
|
+
v_grid,
|
|
29
|
+
halton_tri,
|
|
30
|
+
halton_u,
|
|
31
|
+
halton_v,
|
|
32
|
+
halton_r1,
|
|
33
|
+
halton_r2,
|
|
34
|
+
cdf,
|
|
35
|
+
tri_a,
|
|
36
|
+
tri_e1,
|
|
37
|
+
tri_e2,
|
|
38
|
+
tri_u,
|
|
39
|
+
tri_v,
|
|
40
|
+
tri_n,
|
|
41
|
+
tri_origin_eps,
|
|
42
|
+
rays_per_cell,
|
|
43
|
+
orig,
|
|
44
|
+
dire,
|
|
45
|
+
cp_grid,
|
|
46
|
+
cp_dims,
|
|
47
|
+
):
|
|
48
|
+
"""Create ray origins and directions from precomputed emitter data."""
|
|
49
|
+
n_rays = orig.shape[0]
|
|
50
|
+
two_pi = 6.283185307179586
|
|
51
|
+
|
|
52
|
+
for idx in nb.prange(n_rays):
|
|
53
|
+
cell = idx // rays_per_cell
|
|
54
|
+
ug = (u_grid[cell] + cp_grid[0]) % 1.0
|
|
55
|
+
vg = (v_grid[cell] + cp_grid[1]) % 1.0
|
|
56
|
+
|
|
57
|
+
q_tri = (halton_tri[idx] + cp_dims[0]) % 1.0
|
|
58
|
+
tri = _binary_search_cdf(cdf, q_tri)
|
|
59
|
+
|
|
60
|
+
ur = (halton_u[idx] + cp_dims[1] + ug) % 1.0
|
|
61
|
+
vr = (halton_v[idx] + cp_dims[2] + vg) % 1.0
|
|
62
|
+
|
|
63
|
+
s = math.sqrt(ur)
|
|
64
|
+
mix_b = s * vr
|
|
65
|
+
mix_c = s * (1.0 - vr)
|
|
66
|
+
|
|
67
|
+
ax = tri_a[tri, 0]
|
|
68
|
+
ay = tri_a[tri, 1]
|
|
69
|
+
az = tri_a[tri, 2]
|
|
70
|
+
|
|
71
|
+
px = ax + mix_b * tri_e1[tri, 0] + mix_c * tri_e2[tri, 0]
|
|
72
|
+
py = ay + mix_b * tri_e1[tri, 1] + mix_c * tri_e2[tri, 1]
|
|
73
|
+
pz = az + mix_b * tri_e1[tri, 2] + mix_c * tri_e2[tri, 2]
|
|
74
|
+
|
|
75
|
+
r1 = (halton_r1[idx] + cp_dims[3]) % 1.0
|
|
76
|
+
r2 = (halton_r2[idx] + cp_dims[4]) % 1.0
|
|
77
|
+
|
|
78
|
+
sin_t = math.sqrt(1.0 - r1)
|
|
79
|
+
phi = two_pi * r2
|
|
80
|
+
x = sin_t * math.cos(phi)
|
|
81
|
+
y = sin_t * math.sin(phi)
|
|
82
|
+
z = math.sqrt(r1)
|
|
83
|
+
|
|
84
|
+
dx = x * tri_u[tri, 0] + y * tri_v[tri, 0] + z * tri_n[tri, 0]
|
|
85
|
+
dy = x * tri_u[tri, 1] + y * tri_v[tri, 1] + z * tri_n[tri, 1]
|
|
86
|
+
dz = x * tri_u[tri, 2] + y * tri_v[tri, 2] + z * tri_n[tri, 2]
|
|
87
|
+
eps = tri_origin_eps[tri]
|
|
88
|
+
|
|
89
|
+
orig[idx, 0] = px + eps * tri_n[tri, 0]
|
|
90
|
+
orig[idx, 1] = py + eps * tri_n[tri, 1]
|
|
91
|
+
orig[idx, 2] = pz + eps * tri_n[tri, 2]
|
|
92
|
+
dire[idx, 0] = dx
|
|
93
|
+
dire[idx, 1] = dy
|
|
94
|
+
dire[idx, 2] = dz
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["build_rays"]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: raystrack
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Lightweight Monte-Carlo view-factor solver with CPU, CUDA and BVH paths
|
|
5
|
+
Author-email: Philip Balizki <philip@metis.earth>
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://github.com/philip-ba/raystrack
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Requires-Python: <3.13,>=3.9
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: numpy<3.0,>=1.24
|
|
16
|
+
Requires-Dist: numba<0.60,>=0.59
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Raystrack
|
|
20
|
+
|
|
21
|
+
<p align="left">
|
|
22
|
+
<img src="raystrack_icon.svg" alt="Raystrack icon" width="160">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
Lightweight Monte-Carlo view-factor solver for polygonal meshes.
|
|
26
|
+
|
|
27
|
+
Raystrack computes radiative view factors F(i->j) between triangulated surfaces
|
|
28
|
+
using quasi-Monte-Carlo ray tracing. It runs on CPU, can leverage Numba/CUDA on
|
|
29
|
+
NVIDIA GPUs when available, and optionally accelerates ray intersection with a
|
|
30
|
+
BVH. The repository also ships a pure-Python API you can use outside Rhino.
|
|
31
|
+
|
|
32
|
+
## Features
|
|
33
|
+
- Efficient Monte-Carlo view factors: front/back hits, optional reciprocity
|
|
34
|
+
- CPU and optional CUDA GPU backends (Numba)
|
|
35
|
+
- Optional BVH acceleration structures
|
|
36
|
+
- Python API plus Grasshopper components (Rhino installer coming soon)
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
### Python package
|
|
41
|
+
Use Raystrack as a normal Python package outside Rhino or Grasshopper.
|
|
42
|
+
|
|
43
|
+
From a local clone of this repository:
|
|
44
|
+
```
|
|
45
|
+
pip install .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or from an absolute path:
|
|
49
|
+
```
|
|
50
|
+
pip install /path/to/raystrack
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Requirements: Python 3.9+, `numpy`, `numba`. CUDA acceleration is enabled
|
|
54
|
+
automatically when `numba.cuda` detects a compatible GPU.
|
|
55
|
+
|
|
56
|
+
### Rhino / Grasshopper status
|
|
57
|
+
The Rhino 8 Grasshopper installers are not yet published. Follow this README or the issue tracker for updates when a
|
|
58
|
+
tested Rhino workflow becomes available.
|
|
59
|
+
|
|
60
|
+
## Examples
|
|
61
|
+
|
|
62
|
+
All examples live in `examples/`. Start by running `ex00_street_canyon_geometry`
|
|
63
|
+
to generate `street_canyon.json`; subsequent scripts expect that file. Each
|
|
64
|
+
Python example now documents its own inputs and parameters inline, so open the
|
|
65
|
+
scripts directly for usage guidance and tunable options.
|
|
66
|
+
|
|
67
|
+
## Quick start (Python)
|
|
68
|
+
```python
|
|
69
|
+
import numpy as np
|
|
70
|
+
from raystrack import view_factor_matrix, MatrixParams
|
|
71
|
+
|
|
72
|
+
# Each mesh is a tuple: (name: str, V: (N,3) float32, F: (M,3) int32)
|
|
73
|
+
V_a = np.array([[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0]], dtype=np.float32)
|
|
74
|
+
F_a = np.array([[0, 1, 2], [0, 2, 3]], dtype=np.int32) # two triangles
|
|
75
|
+
|
|
76
|
+
V_b = np.array([[0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]], dtype=np.float32)
|
|
77
|
+
F_b = F_a.copy()
|
|
78
|
+
|
|
79
|
+
meshes = [
|
|
80
|
+
("A", V_a, F_a),
|
|
81
|
+
("B", V_b, F_b),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
params = MatrixParams(
|
|
85
|
+
samples=256, # sampling density per unit area (QMC grid)
|
|
86
|
+
rays=256, # rays per cell
|
|
87
|
+
bvh="builtin", # optional BVH acceleration (auto|off|builtin)
|
|
88
|
+
reciprocity=True,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
res = view_factor_matrix(meshes, params=params)
|
|
92
|
+
|
|
93
|
+
print(res["A"]) # e.g. {"B_front": 0.5, "B_back": 0.0, ...}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
To persist a matrix without directional suffixes in the saved JSON:
|
|
97
|
+
```python
|
|
98
|
+
from raystrack import save_vf_matrix_json
|
|
99
|
+
|
|
100
|
+
save_vf_matrix_json(res, "vf_matrix.json", strip_dir=True)
|
|
101
|
+
```
|
|
102
|
+
This collapses receiver keys like `"B_front"` and `"B_back"` into `"B"` and
|
|
103
|
+
sums both values per sender row.
|
|
104
|
+
|
|
105
|
+
## Parameter presets
|
|
106
|
+
Raystrack uses two parameter containers to keep configuration consistent:
|
|
107
|
+
- `MatrixParams`: controls the scene-to-scene view-factor solve (sampling, BVH,
|
|
108
|
+
device selection, convergence tolerances, and reciprocity enforcement).
|
|
109
|
+
- `SkyParams`: controls the sky view-factor solve (sampling, device selection,
|
|
110
|
+
and convergence tolerances, plus `discrete=True` for 145 sky patches or
|
|
111
|
+
`False` for a single merged `"Sky"` output).
|
|
112
|
+
|
|
113
|
+
Typical usage:
|
|
114
|
+
```python
|
|
115
|
+
from raystrack import MatrixParams, SkyParams, view_factor_matrix, view_factor_to_tregenza_sky
|
|
116
|
+
|
|
117
|
+
matrix_params = MatrixParams(samples=32, rays=256, reciprocity=True, flip_faces=False)
|
|
118
|
+
sky_params = SkyParams(samples=32, rays=256)
|
|
119
|
+
|
|
120
|
+
vf_scene = view_factor_matrix(meshes, params=matrix_params)
|
|
121
|
+
vf_sky = view_factor_to_tregenza_sky(meshes, params=sky_params)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
GPL-3.0-only - see `LICENSE`.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
raystrack/__init__.py,sha256=QVC1j713ZEl4BcWD-CDvQmHmXKbj2_fUAGsIhW_kt1M,677
|
|
2
|
+
raystrack/api.py,sha256=yvtM7LoKZhihAZrziwMWgSB5NHdLUXiZ-Kx099gyKPY,7742
|
|
3
|
+
raystrack/io.py,sha256=CsU6pSplFqYUBwPHOXuU48IKrTgfFoyc0j-N5BMUic8,8154
|
|
4
|
+
raystrack/main.py,sha256=U3LgHHiwz5dHsZfssE50vGQLDQPu_ZArpt0nsQ9KwUA,91750
|
|
5
|
+
raystrack/params.py,sha256=bIwRfzb0sdTDEwvldDIRg8xz529zA2l2cKDvJdS1-6Y,4313
|
|
6
|
+
raystrack/utils/__init__.py,sha256=hJ0nNPOGhBbYQOxdiOLca3kjc6XK-897cPqmnGpjHiM,702
|
|
7
|
+
raystrack/utils/bvh.py,sha256=H-nGkBMjvFxuhWHoksz0cmiRS5mxnEU36szMTPmLcTQ,2283
|
|
8
|
+
raystrack/utils/cpu_trace.py,sha256=-Jztd6moL0-3QseJ1XBNbwPZrrbU7nxiYnwakr4LJfE,22785
|
|
9
|
+
raystrack/utils/cuda_trace.py,sha256=lIgPG9h-HocPWZlJ0UFyAOCEbXznRNqvDTFPEo4SLoo,34636
|
|
10
|
+
raystrack/utils/geometry.py,sha256=D3ABPcrYDJZXxAGiw3MuLPTPaKRPKr-w52ygxcu1YzQ,2151
|
|
11
|
+
raystrack/utils/halton.py,sha256=2_ShczxX-WTpZpOLLiJPAdlNuu3rNKSo7Zw7tRDieCQ,1467
|
|
12
|
+
raystrack/utils/helpers.py,sha256=Hu4f-2QRU_8lWa74DUXA2u9HPgCbr3m2cK58KSER_N8,8995
|
|
13
|
+
raystrack/utils/prepared.py,sha256=FvOSwN_3zCktBEJdKl223qe7cZ_0nCb7Gpu3mpzVDiE,15219
|
|
14
|
+
raystrack/utils/ray_builder.py,sha256=IV-81KdGxuYfqZcRHhwtoMYDLvH9zFxYBcgAT4Y76Bw,2372
|
|
15
|
+
raystrack-1.0.1.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
16
|
+
raystrack-1.0.1.dist-info/METADATA,sha256=vpyrXC_CJWWzIsUVLE5TzjNGuhKmujNjmEupHpRkNgw,4226
|
|
17
|
+
raystrack-1.0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
18
|
+
raystrack-1.0.1.dist-info/top_level.txt,sha256=tLhnPDw74DVcYVMnJeqw8bPMFLURvyR756B05cQrJKE,10
|
|
19
|
+
raystrack-1.0.1.dist-info/RECORD,,
|