voxcity 1.0.2__py3-none-any.whl → 1.0.15__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.
Files changed (41) hide show
  1. voxcity/downloader/ocean.py +559 -0
  2. voxcity/generator/api.py +6 -0
  3. voxcity/generator/grids.py +45 -32
  4. voxcity/generator/pipeline.py +327 -27
  5. voxcity/geoprocessor/draw.py +14 -8
  6. voxcity/geoprocessor/raster/__init__.py +2 -0
  7. voxcity/geoprocessor/raster/core.py +31 -0
  8. voxcity/geoprocessor/raster/landcover.py +173 -49
  9. voxcity/geoprocessor/raster/raster.py +1 -1
  10. voxcity/models.py +2 -0
  11. voxcity/simulator/solar/__init__.py +13 -0
  12. voxcity/simulator_gpu/__init__.py +90 -0
  13. voxcity/simulator_gpu/core.py +322 -0
  14. voxcity/simulator_gpu/domain.py +36 -0
  15. voxcity/simulator_gpu/init_taichi.py +154 -0
  16. voxcity/simulator_gpu/raytracing.py +776 -0
  17. voxcity/simulator_gpu/solar/__init__.py +222 -0
  18. voxcity/simulator_gpu/solar/core.py +66 -0
  19. voxcity/simulator_gpu/solar/csf.py +1249 -0
  20. voxcity/simulator_gpu/solar/domain.py +618 -0
  21. voxcity/simulator_gpu/solar/epw.py +421 -0
  22. voxcity/simulator_gpu/solar/integration.py +4322 -0
  23. voxcity/simulator_gpu/solar/mask.py +459 -0
  24. voxcity/simulator_gpu/solar/radiation.py +3019 -0
  25. voxcity/simulator_gpu/solar/raytracing.py +182 -0
  26. voxcity/simulator_gpu/solar/reflection.py +533 -0
  27. voxcity/simulator_gpu/solar/sky.py +907 -0
  28. voxcity/simulator_gpu/solar/solar.py +337 -0
  29. voxcity/simulator_gpu/solar/svf.py +446 -0
  30. voxcity/simulator_gpu/solar/volumetric.py +2099 -0
  31. voxcity/simulator_gpu/visibility/__init__.py +109 -0
  32. voxcity/simulator_gpu/visibility/geometry.py +278 -0
  33. voxcity/simulator_gpu/visibility/integration.py +808 -0
  34. voxcity/simulator_gpu/visibility/landmark.py +753 -0
  35. voxcity/simulator_gpu/visibility/view.py +944 -0
  36. voxcity/visualizer/renderer.py +2 -1
  37. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/METADATA +16 -53
  38. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/RECORD +41 -16
  39. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/WHEEL +0 -0
  40. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/licenses/AUTHORS.rst +0 -0
  41. {voxcity-1.0.2.dist-info → voxcity-1.0.15.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,776 @@
1
+ """
2
+ Shared ray tracing module for simulator_gpu.
3
+
4
+ Implements GPU-accelerated ray tracing through a 3D voxel grid
5
+ for both solar radiation and view analysis.
6
+ Uses 3D-DDA (Digital Differential Analyzer) for voxel traversal.
7
+
8
+ Key features:
9
+ - Beer-Lambert law for canopy: trans = exp(-ext_coef * LAD * path_length)
10
+ - Solid obstacles block rays completely (trans = 0)
11
+ - Tree canopy attenuates rays based on LAD and path length
12
+ """
13
+
14
+ import taichi as ti
15
+ import math
16
+ from typing import Tuple, Optional
17
+
18
+ from .core import Vector3, Point3, EXT_COEF
19
+
20
+
21
+ @ti.func
22
+ def ray_aabb_intersect(
23
+ ray_origin: Vector3,
24
+ ray_dir: Vector3,
25
+ box_min: Vector3,
26
+ box_max: Vector3,
27
+ t_min: ti.f32,
28
+ t_max: ti.f32
29
+ ):
30
+ """
31
+ Ray-AABB intersection using slab method.
32
+
33
+ Args:
34
+ ray_origin: Ray origin point
35
+ ray_dir: Ray direction (normalized)
36
+ box_min: AABB minimum corner
37
+ box_max: AABB maximum corner
38
+ t_min: Minimum t value
39
+ t_max: Maximum t value
40
+
41
+ Returns:
42
+ Tuple of (hit, t_enter, t_exit)
43
+ """
44
+ t_enter = t_min
45
+ t_exit = t_max
46
+ hit = 1
47
+
48
+ for i in ti.static(range(3)):
49
+ if ti.abs(ray_dir[i]) < 1e-10:
50
+ # Ray parallel to slab
51
+ if ray_origin[i] < box_min[i] or ray_origin[i] > box_max[i]:
52
+ hit = 0
53
+ else:
54
+ inv_d = 1.0 / ray_dir[i]
55
+ t1 = (box_min[i] - ray_origin[i]) * inv_d
56
+ t2 = (box_max[i] - ray_origin[i]) * inv_d
57
+
58
+ if t1 > t2:
59
+ t1, t2 = t2, t1
60
+
61
+ t_enter = ti.max(t_enter, t1)
62
+ t_exit = ti.min(t_exit, t2)
63
+
64
+ if t_enter > t_exit:
65
+ hit = 0
66
+
67
+ return hit, t_enter, t_exit
68
+
69
+
70
+ @ti.func
71
+ def ray_voxel_first_hit(
72
+ ray_origin: Vector3,
73
+ ray_dir: Vector3,
74
+ is_solid: ti.template(),
75
+ nx: ti.i32,
76
+ ny: ti.i32,
77
+ nz: ti.i32,
78
+ dx: ti.f32,
79
+ dy: ti.f32,
80
+ dz: ti.f32,
81
+ max_dist: ti.f32
82
+ ):
83
+ """
84
+ 3D-DDA ray marching to find first solid voxel hit.
85
+
86
+ Args:
87
+ ray_origin: Ray origin
88
+ ray_dir: Ray direction (normalized)
89
+ is_solid: 3D field of solid cells
90
+ nx, ny, nz: Grid dimensions
91
+ dx, dy, dz: Cell sizes
92
+ max_dist: Maximum ray distance
93
+
94
+ Returns:
95
+ Tuple of (hit, t_hit, ix, iy, iz)
96
+ """
97
+ hit = 0
98
+ t_hit = max_dist
99
+ hit_ix, hit_iy, hit_iz = 0, 0, 0
100
+
101
+ # Find entry into domain
102
+ domain_min = Vector3(0.0, 0.0, 0.0)
103
+ domain_max = Vector3(nx * dx, ny * dy, nz * dz)
104
+
105
+ in_domain, t_enter, t_exit = ray_aabb_intersect(
106
+ ray_origin, ray_dir, domain_min, domain_max, 0.0, max_dist
107
+ )
108
+
109
+ if in_domain == 1:
110
+ # Start position (slightly inside domain)
111
+ t = t_enter + 1e-5
112
+ pos = ray_origin + ray_dir * t
113
+
114
+ # Current voxel indices
115
+ ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
116
+ iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
117
+ iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
118
+
119
+ # Clamp to valid range
120
+ ix = ti.max(0, ti.min(nx - 1, ix))
121
+ iy = ti.max(0, ti.min(ny - 1, iy))
122
+ iz = ti.max(0, ti.min(nz - 1, iz))
123
+
124
+ # Step directions
125
+ step_x = 1 if ray_dir[0] >= 0 else -1
126
+ step_y = 1 if ray_dir[1] >= 0 else -1
127
+ step_z = 1 if ray_dir[2] >= 0 else -1
128
+
129
+ # Initialize DDA variables
130
+ t_max_x = 1e30
131
+ t_max_y = 1e30
132
+ t_max_z = 1e30
133
+ t_delta_x = 1e30
134
+ t_delta_y = 1e30
135
+ t_delta_z = 1e30
136
+
137
+ # t values for next boundary crossing
138
+ if ti.abs(ray_dir[0]) > 1e-10:
139
+ if step_x > 0:
140
+ t_max_x = ((ix + 1) * dx - pos[0]) / ray_dir[0] + t
141
+ else:
142
+ t_max_x = (ix * dx - pos[0]) / ray_dir[0] + t
143
+ t_delta_x = ti.abs(dx / ray_dir[0])
144
+
145
+ if ti.abs(ray_dir[1]) > 1e-10:
146
+ if step_y > 0:
147
+ t_max_y = ((iy + 1) * dy - pos[1]) / ray_dir[1] + t
148
+ else:
149
+ t_max_y = (iy * dy - pos[1]) / ray_dir[1] + t
150
+ t_delta_y = ti.abs(dy / ray_dir[1])
151
+
152
+ if ti.abs(ray_dir[2]) > 1e-10:
153
+ if step_z > 0:
154
+ t_max_z = ((iz + 1) * dz - pos[2]) / ray_dir[2] + t
155
+ else:
156
+ t_max_z = (iz * dz - pos[2]) / ray_dir[2] + t
157
+ t_delta_z = ti.abs(dz / ray_dir[2])
158
+
159
+ # 3D-DDA traversal - optimized with done flag to reduce branch divergence
160
+ max_steps = nx + ny + nz
161
+ done = 0
162
+
163
+ for _ in range(max_steps):
164
+ if done == 0:
165
+ # Bounds check - exit if outside domain
166
+ if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
167
+ done = 1
168
+ elif t > t_exit:
169
+ done = 1
170
+ # Check current voxel for solid hit
171
+ elif is_solid[ix, iy, iz] == 1:
172
+ hit = 1
173
+ t_hit = t
174
+ hit_ix = ix
175
+ hit_iy = iy
176
+ hit_iz = iz
177
+ done = 1
178
+ else:
179
+ # Step to next voxel using branchless min selection
180
+ if t_max_x < t_max_y and t_max_x < t_max_z:
181
+ t = t_max_x
182
+ ix += step_x
183
+ t_max_x += t_delta_x
184
+ elif t_max_y < t_max_z:
185
+ t = t_max_y
186
+ iy += step_y
187
+ t_max_y += t_delta_y
188
+ else:
189
+ t = t_max_z
190
+ iz += step_z
191
+ t_max_z += t_delta_z
192
+
193
+ return hit, t_hit, hit_ix, hit_iy, hit_iz
194
+
195
+
196
+ @ti.func
197
+ def ray_voxel_transmissivity(
198
+ ray_origin: Vector3,
199
+ ray_dir: Vector3,
200
+ is_solid: ti.template(),
201
+ is_tree: ti.template(),
202
+ nx: ti.i32,
203
+ ny: ti.i32,
204
+ nz: ti.i32,
205
+ dx: ti.f32,
206
+ dy: ti.f32,
207
+ dz: ti.f32,
208
+ max_dist: ti.f32,
209
+ tree_k: ti.f32,
210
+ tree_lad: ti.f32
211
+ ):
212
+ """
213
+ 3D-DDA ray marching with tree canopy transmissivity calculation.
214
+
215
+ Args:
216
+ ray_origin: Ray origin
217
+ ray_dir: Ray direction (normalized)
218
+ is_solid: 3D field of solid cells (buildings, ground)
219
+ is_tree: 3D field of tree cells
220
+ nx, ny, nz: Grid dimensions
221
+ dx, dy, dz: Cell sizes
222
+ max_dist: Maximum ray distance
223
+ tree_k: Tree extinction coefficient
224
+ tree_lad: Leaf area density
225
+
226
+ Returns:
227
+ Tuple of (blocked_by_solid, transmissivity)
228
+ - blocked_by_solid: 1 if ray hit solid, 0 otherwise
229
+ - transmissivity: 0-1 fraction of light that gets through trees
230
+ """
231
+ blocked = 0
232
+ transmissivity = 1.0
233
+
234
+ # Find entry into domain
235
+ domain_min = Vector3(0.0, 0.0, 0.0)
236
+ domain_max = Vector3(nx * dx, ny * dy, nz * dz)
237
+
238
+ in_domain, t_enter, t_exit = ray_aabb_intersect(
239
+ ray_origin, ray_dir, domain_min, domain_max, 0.0, max_dist
240
+ )
241
+
242
+ if in_domain == 1:
243
+ t = t_enter + 1e-5
244
+ pos = ray_origin + ray_dir * t
245
+
246
+ ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
247
+ iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
248
+ iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
249
+
250
+ ix = ti.max(0, ti.min(nx - 1, ix))
251
+ iy = ti.max(0, ti.min(ny - 1, iy))
252
+ iz = ti.max(0, ti.min(nz - 1, iz))
253
+
254
+ step_x = 1 if ray_dir[0] >= 0 else -1
255
+ step_y = 1 if ray_dir[1] >= 0 else -1
256
+ step_z = 1 if ray_dir[2] >= 0 else -1
257
+
258
+ t_max_x = 1e30
259
+ t_max_y = 1e30
260
+ t_max_z = 1e30
261
+ t_delta_x = 1e30
262
+ t_delta_y = 1e30
263
+ t_delta_z = 1e30
264
+
265
+ if ti.abs(ray_dir[0]) > 1e-10:
266
+ if step_x > 0:
267
+ t_max_x = ((ix + 1) * dx - pos[0]) / ray_dir[0] + t
268
+ else:
269
+ t_max_x = (ix * dx - pos[0]) / ray_dir[0] + t
270
+ t_delta_x = ti.abs(dx / ray_dir[0])
271
+
272
+ if ti.abs(ray_dir[1]) > 1e-10:
273
+ if step_y > 0:
274
+ t_max_y = ((iy + 1) * dy - pos[1]) / ray_dir[1] + t
275
+ else:
276
+ t_max_y = (iy * dy - pos[1]) / ray_dir[1] + t
277
+ t_delta_y = ti.abs(dy / ray_dir[1])
278
+
279
+ if ti.abs(ray_dir[2]) > 1e-10:
280
+ if step_z > 0:
281
+ t_max_z = ((iz + 1) * dz - pos[2]) / ray_dir[2] + t
282
+ else:
283
+ t_max_z = (iz * dz - pos[2]) / ray_dir[2] + t
284
+ t_delta_z = ti.abs(dz / ray_dir[2])
285
+
286
+ t_prev = t
287
+ max_steps = nx + ny + nz
288
+ done = 0
289
+
290
+ for _ in range(max_steps):
291
+ if done == 0:
292
+ if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
293
+ done = 1
294
+ elif t > t_exit:
295
+ done = 1
296
+ elif is_solid[ix, iy, iz] == 1:
297
+ blocked = 1
298
+ transmissivity = 0.0
299
+ done = 1
300
+ else:
301
+ # Get step distance
302
+ t_next = ti.min(t_max_x, ti.min(t_max_y, t_max_z))
303
+
304
+ # Path length through this cell
305
+ path_len = t_next - t_prev
306
+
307
+ # Accumulate absorption from tree canopy
308
+ if is_tree[ix, iy, iz] == 1:
309
+ # Beer-Lambert: T = exp(-k * LAD * path)
310
+ segment_trans = ti.exp(-tree_k * tree_lad * path_len)
311
+ transmissivity *= segment_trans
312
+
313
+ # Early termination if transmissivity is negligible
314
+ if transmissivity < 0.01:
315
+ done = 1
316
+
317
+ t_prev = t_next
318
+
319
+ # Step to next voxel
320
+ if t_max_x < t_max_y and t_max_x < t_max_z:
321
+ t = t_max_x
322
+ ix += step_x
323
+ t_max_x += t_delta_x
324
+ elif t_max_y < t_max_z:
325
+ t = t_max_y
326
+ iy += step_y
327
+ t_max_y += t_delta_y
328
+ else:
329
+ t = t_max_z
330
+ iz += step_z
331
+ t_max_z += t_delta_z
332
+
333
+ return blocked, transmissivity
334
+
335
+
336
+ @ti.func
337
+ def ray_canopy_absorption(
338
+ ray_origin: Vector3,
339
+ ray_dir: Vector3,
340
+ lad: ti.template(),
341
+ is_solid: ti.template(),
342
+ nx: ti.i32,
343
+ ny: ti.i32,
344
+ nz: ti.i32,
345
+ dx: ti.f32,
346
+ dy: ti.f32,
347
+ dz: ti.f32,
348
+ max_dist: ti.f32,
349
+ ext_coef: ti.f32
350
+ ):
351
+ """
352
+ Trace ray through canopy computing Beer-Lambert absorption.
353
+
354
+ Args:
355
+ ray_origin: Ray origin
356
+ ray_dir: Ray direction (normalized)
357
+ lad: 3D field of Leaf Area Density
358
+ is_solid: 3D field of solid cells (buildings/terrain)
359
+ nx, ny, nz: Grid dimensions
360
+ dx, dy, dz: Cell sizes
361
+ max_dist: Maximum ray distance
362
+ ext_coef: Extinction coefficient
363
+
364
+ Returns:
365
+ Tuple of (transmissivity, path_length_through_canopy)
366
+ """
367
+ transmissivity = 1.0
368
+ total_lad_path = 0.0
369
+
370
+ # Find entry into domain
371
+ domain_min = Vector3(0.0, 0.0, 0.0)
372
+ domain_max = Vector3(nx * dx, ny * dy, nz * dz)
373
+
374
+ in_domain, t_enter, t_exit = ray_aabb_intersect(
375
+ ray_origin, ray_dir, domain_min, domain_max, 0.0, max_dist
376
+ )
377
+
378
+ if in_domain == 1:
379
+ t = t_enter + 1e-5
380
+ pos = ray_origin + ray_dir * t
381
+
382
+ ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
383
+ iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
384
+ iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
385
+
386
+ ix = ti.max(0, ti.min(nx - 1, ix))
387
+ iy = ti.max(0, ti.min(ny - 1, iy))
388
+ iz = ti.max(0, ti.min(nz - 1, iz))
389
+
390
+ step_x = 1 if ray_dir[0] >= 0 else -1
391
+ step_y = 1 if ray_dir[1] >= 0 else -1
392
+ step_z = 1 if ray_dir[2] >= 0 else -1
393
+
394
+ t_max_x = 1e30
395
+ t_max_y = 1e30
396
+ t_max_z = 1e30
397
+ t_delta_x = 1e30
398
+ t_delta_y = 1e30
399
+ t_delta_z = 1e30
400
+
401
+ if ti.abs(ray_dir[0]) > 1e-10:
402
+ if step_x > 0:
403
+ t_max_x = ((ix + 1) * dx - pos[0]) / ray_dir[0] + t
404
+ else:
405
+ t_max_x = (ix * dx - pos[0]) / ray_dir[0] + t
406
+ t_delta_x = ti.abs(dx / ray_dir[0])
407
+
408
+ if ti.abs(ray_dir[1]) > 1e-10:
409
+ if step_y > 0:
410
+ t_max_y = ((iy + 1) * dy - pos[1]) / ray_dir[1] + t
411
+ else:
412
+ t_max_y = (iy * dy - pos[1]) / ray_dir[1] + t
413
+ t_delta_y = ti.abs(dy / ray_dir[1])
414
+
415
+ if ti.abs(ray_dir[2]) > 1e-10:
416
+ if step_z > 0:
417
+ t_max_z = ((iz + 1) * dz - pos[2]) / ray_dir[2] + t
418
+ else:
419
+ t_max_z = (iz * dz - pos[2]) / ray_dir[2] + t
420
+ t_delta_z = ti.abs(dz / ray_dir[2])
421
+
422
+ t_prev = t
423
+ max_steps = nx + ny + nz
424
+ done = 0
425
+
426
+ for _ in range(max_steps):
427
+ if done == 0:
428
+ if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
429
+ done = 1
430
+ elif t > t_exit:
431
+ done = 1
432
+ elif is_solid[ix, iy, iz] == 1:
433
+ transmissivity = 0.0
434
+ done = 1
435
+ else:
436
+ # Get step distance
437
+ t_next = ti.min(t_max_x, ti.min(t_max_y, t_max_z))
438
+
439
+ # Path length through this cell
440
+ path_len = t_next - t_prev
441
+
442
+ # Accumulate absorption from LAD
443
+ cell_lad = lad[ix, iy, iz]
444
+ if cell_lad > 0.0:
445
+ lad_path = cell_lad * path_len
446
+ total_lad_path += lad_path
447
+ # Beer-Lambert: T = exp(-ext_coef * LAD * path)
448
+ transmissivity *= ti.exp(-ext_coef * lad_path)
449
+
450
+ t_prev = t_next
451
+
452
+ # Step to next voxel
453
+ if t_max_x < t_max_y and t_max_x < t_max_z:
454
+ t = t_max_x
455
+ ix += step_x
456
+ t_max_x += t_delta_x
457
+ elif t_max_y < t_max_z:
458
+ t = t_max_y
459
+ iy += step_y
460
+ t_max_y += t_delta_y
461
+ else:
462
+ t = t_max_z
463
+ iz += step_z
464
+ t_max_z += t_delta_z
465
+
466
+ return transmissivity, total_lad_path
467
+
468
+
469
+ @ti.func
470
+ def ray_point_to_point_transmissivity(
471
+ pos_from: Vector3,
472
+ pos_to: Vector3,
473
+ lad: ti.template(),
474
+ is_solid: ti.template(),
475
+ nx: ti.i32,
476
+ ny: ti.i32,
477
+ nz: ti.i32,
478
+ dx: ti.f32,
479
+ dy: ti.f32,
480
+ dz: ti.f32,
481
+ ext_coef: ti.f32
482
+ ):
483
+ """
484
+ Compute transmissivity of radiation between two points through canopy.
485
+
486
+ This is used for surface-to-surface reflections where reflected radiation
487
+ must pass through any intervening vegetation.
488
+
489
+ Args:
490
+ pos_from: Start position (emitting surface center)
491
+ pos_to: End position (receiving surface center)
492
+ lad: 3D field of Leaf Area Density
493
+ is_solid: 3D field of solid cells (buildings/terrain)
494
+ nx, ny, nz: Grid dimensions
495
+ dx, dy, dz: Cell sizes
496
+ ext_coef: Extinction coefficient
497
+
498
+ Returns:
499
+ Tuple of (transmissivity, blocked_by_solid)
500
+ - transmissivity: 0-1 fraction of radiation that gets through
501
+ - blocked_by_solid: 1 if ray hits a solid cell, 0 otherwise
502
+ """
503
+ # Compute ray direction and distance
504
+ diff = pos_to - pos_from
505
+ dist = diff.norm()
506
+
507
+ transmissivity = 1.0
508
+ blocked = 0
509
+
510
+ # Only trace if distance is significant
511
+ if dist >= 0.01:
512
+ ray_dir = diff / dist
513
+
514
+ # Starting voxel
515
+ pos = pos_from + ray_dir * 0.01 # Slight offset to avoid self-intersection
516
+
517
+ ix = ti.cast(ti.floor(pos[0] / dx), ti.i32)
518
+ iy = ti.cast(ti.floor(pos[1] / dy), ti.i32)
519
+ iz = ti.cast(ti.floor(pos[2] / dz), ti.i32)
520
+
521
+ # Clamp to valid range
522
+ ix = ti.max(0, ti.min(nx - 1, ix))
523
+ iy = ti.max(0, ti.min(ny - 1, iy))
524
+ iz = ti.max(0, ti.min(nz - 1, iz))
525
+
526
+ # Step directions
527
+ step_x = 1 if ray_dir[0] >= 0 else -1
528
+ step_y = 1 if ray_dir[1] >= 0 else -1
529
+ step_z = 1 if ray_dir[2] >= 0 else -1
530
+
531
+ # Initialize DDA variables
532
+ t_max_x = 1e30
533
+ t_max_y = 1e30
534
+ t_max_z = 1e30
535
+ t_delta_x = 1e30
536
+ t_delta_y = 1e30
537
+ t_delta_z = 1e30
538
+
539
+ t = 0.01 # Start offset
540
+
541
+ if ti.abs(ray_dir[0]) > 1e-10:
542
+ if step_x > 0:
543
+ t_max_x = ((ix + 1) * dx - pos_from[0]) / ray_dir[0]
544
+ else:
545
+ t_max_x = (ix * dx - pos_from[0]) / ray_dir[0]
546
+ t_delta_x = ti.abs(dx / ray_dir[0])
547
+
548
+ if ti.abs(ray_dir[1]) > 1e-10:
549
+ if step_y > 0:
550
+ t_max_y = ((iy + 1) * dy - pos_from[1]) / ray_dir[1]
551
+ else:
552
+ t_max_y = (iy * dy - pos_from[1]) / ray_dir[1]
553
+ t_delta_y = ti.abs(dy / ray_dir[1])
554
+
555
+ if ti.abs(ray_dir[2]) > 1e-10:
556
+ if step_z > 0:
557
+ t_max_z = ((iz + 1) * dz - pos_from[2]) / ray_dir[2]
558
+ else:
559
+ t_max_z = (iz * dz - pos_from[2]) / ray_dir[2]
560
+ t_delta_z = ti.abs(dz / ray_dir[2])
561
+
562
+ t_prev = t
563
+ max_steps = nx + ny + nz
564
+ done = 0
565
+
566
+ for _ in range(max_steps):
567
+ if done == 1:
568
+ continue # Skip remaining iterations
569
+
570
+ if ix < 0 or ix >= nx or iy < 0 or iy >= ny or iz < 0 or iz >= nz:
571
+ done = 1
572
+ continue
573
+ if t > dist: # Reached target
574
+ done = 1
575
+ continue
576
+
577
+ # Check for solid obstruction (but skip first and last cell as they're the surfaces)
578
+ if is_solid[ix, iy, iz] == 1 and t > 0.1 and t < dist - 0.1:
579
+ blocked = 1
580
+ transmissivity = 0.0
581
+ done = 1
582
+ continue
583
+
584
+ # Get step distance
585
+ t_next = t_max_x
586
+ if t_max_y < t_next:
587
+ t_next = t_max_y
588
+ if t_max_z < t_next:
589
+ t_next = t_max_z
590
+
591
+ # Limit to target distance
592
+ t_next = ti.min(t_next, dist)
593
+
594
+ # Path length through this cell
595
+ path_len = t_next - t_prev
596
+
597
+ # Accumulate absorption from LAD
598
+ cell_lad = lad[ix, iy, iz]
599
+ if cell_lad > 0.0:
600
+ # Beer-Lambert: T = exp(-ext_coef * LAD * path)
601
+ transmissivity *= ti.exp(-ext_coef * cell_lad * path_len)
602
+
603
+ t_prev = t_next
604
+
605
+ # Step to next voxel
606
+ if t_max_x < t_max_y and t_max_x < t_max_z:
607
+ t = t_max_x
608
+ ix += step_x
609
+ t_max_x += t_delta_x
610
+ elif t_max_y < t_max_z:
611
+ t = t_max_y
612
+ iy += step_y
613
+ t_max_y += t_delta_y
614
+ else:
615
+ t = t_max_z
616
+ iz += step_z
617
+ t_max_z += t_delta_z
618
+
619
+ return transmissivity, blocked
620
+
621
+
622
+ @ti.func
623
+ def ray_trace_to_target(
624
+ origin: Vector3,
625
+ target: Vector3,
626
+ is_solid: ti.template(),
627
+ is_tree: ti.template(),
628
+ nx: ti.i32,
629
+ ny: ti.i32,
630
+ nz: ti.i32,
631
+ dx: ti.f32,
632
+ dy: ti.f32,
633
+ dz: ti.f32,
634
+ tree_att: ti.f32,
635
+ att_cutoff: ti.f32
636
+ ):
637
+ """
638
+ Trace ray from origin to target, checking for visibility.
639
+
640
+ Args:
641
+ origin: Start position (in voxel coordinates)
642
+ target: End position (in voxel coordinates)
643
+ is_solid: 3D field of solid cells
644
+ is_tree: 3D field of tree cells
645
+ nx, ny, nz: Grid dimensions
646
+ dx, dy, dz: Cell sizes (typically all 1.0 for voxel coords)
647
+ tree_att: Attenuation factor per voxel for trees
648
+ att_cutoff: Minimum transmissivity before considering blocked
649
+
650
+ Returns:
651
+ 1 if target is visible, 0 otherwise
652
+ """
653
+ diff = target - origin
654
+ dist = diff.norm()
655
+
656
+ if dist < 0.01:
657
+ return 1
658
+
659
+ ray_dir = diff / dist
660
+
661
+ x, y, z = origin[0] + 0.5, origin[1] + 0.5, origin[2] + 0.5
662
+ i = ti.cast(ti.floor(origin[0]), ti.i32)
663
+ j = ti.cast(ti.floor(origin[1]), ti.i32)
664
+ k = ti.cast(ti.floor(origin[2]), ti.i32)
665
+
666
+ ti_x = ti.cast(ti.floor(target[0]), ti.i32)
667
+ tj_y = ti.cast(ti.floor(target[1]), ti.i32)
668
+ tk_z = ti.cast(ti.floor(target[2]), ti.i32)
669
+
670
+ step_x = 1 if ray_dir[0] >= 0 else -1
671
+ step_y = 1 if ray_dir[1] >= 0 else -1
672
+ step_z = 1 if ray_dir[2] >= 0 else -1
673
+
674
+ BIG = 1e30
675
+ t_max_x, t_max_y, t_max_z = BIG, BIG, BIG
676
+ t_delta_x, t_delta_y, t_delta_z = BIG, BIG, BIG
677
+
678
+ if ray_dir[0] != 0.0:
679
+ t_max_x = (((i + (1 if step_x > 0 else 0)) - x) / ray_dir[0])
680
+ t_delta_x = ti.abs(1.0 / ray_dir[0])
681
+ if ray_dir[1] != 0.0:
682
+ t_max_y = (((j + (1 if step_y > 0 else 0)) - y) / ray_dir[1])
683
+ t_delta_y = ti.abs(1.0 / ray_dir[1])
684
+ if ray_dir[2] != 0.0:
685
+ t_max_z = (((k + (1 if step_z > 0 else 0)) - z) / ray_dir[2])
686
+ t_delta_z = ti.abs(1.0 / ray_dir[2])
687
+
688
+ T = 1.0
689
+ visible = 1
690
+ max_steps = nx + ny + nz
691
+ done = 0
692
+
693
+ for _ in range(max_steps):
694
+ if done == 0:
695
+ if i < 0 or i >= nx or j < 0 or j >= ny or k < 0 or k >= nz:
696
+ visible = 0
697
+ done = 1
698
+ elif is_solid[i, j, k] == 1:
699
+ visible = 0
700
+ done = 1
701
+ elif is_tree[i, j, k] == 1:
702
+ T *= tree_att
703
+ if T < att_cutoff:
704
+ visible = 0
705
+ done = 1
706
+
707
+ if done == 0:
708
+ # Check if we've reached the target
709
+ if i == ti_x and j == tj_y and k == tk_z:
710
+ done = 1
711
+ else:
712
+ # Step to next voxel
713
+ if t_max_x < t_max_y:
714
+ if t_max_x < t_max_z:
715
+ t_max_x += t_delta_x
716
+ i += step_x
717
+ else:
718
+ t_max_z += t_delta_z
719
+ k += step_z
720
+ else:
721
+ if t_max_y < t_max_z:
722
+ t_max_y += t_delta_y
723
+ j += step_y
724
+ else:
725
+ t_max_z += t_delta_z
726
+ k += step_z
727
+
728
+ return visible
729
+
730
+
731
+ @ti.func
732
+ def sample_hemisphere_direction(i_azim: ti.i32, i_elev: ti.i32, n_azim: ti.i32, n_elev: ti.i32) -> Vector3:
733
+ """
734
+ Generate a direction on the upper hemisphere.
735
+
736
+ Args:
737
+ i_azim: Azimuthal index (0 to n_azim-1)
738
+ i_elev: Elevation index (0 to n_elev-1)
739
+ n_azim: Number of azimuthal divisions
740
+ n_elev: Number of elevation divisions
741
+
742
+ Returns:
743
+ Unit direction vector
744
+ """
745
+ PI = 3.14159265359
746
+
747
+ # Elevation angle (from zenith)
748
+ elev = (i_elev + 0.5) * (PI / 2.0) / n_elev
749
+
750
+ # Azimuth angle
751
+ azim = (i_azim + 0.5) * (2.0 * PI) / n_azim
752
+
753
+ # Convert to Cartesian (z up)
754
+ sin_elev = ti.sin(elev)
755
+ cos_elev = ti.cos(elev)
756
+
757
+ x = sin_elev * ti.sin(azim)
758
+ y = sin_elev * ti.cos(azim)
759
+ z = cos_elev
760
+
761
+ return Vector3(x, y, z)
762
+
763
+
764
+ @ti.func
765
+ def hemisphere_solid_angle(i_elev: ti.i32, n_azim: ti.i32, n_elev: ti.i32) -> ti.f32:
766
+ """
767
+ Calculate solid angle for a hemisphere segment.
768
+ """
769
+ PI = 3.14159265359
770
+
771
+ elev_low = i_elev * (PI / 2.0) / n_elev
772
+ elev_high = (i_elev + 1) * (PI / 2.0) / n_elev
773
+
774
+ d_omega = (2.0 * PI / n_azim) * (ti.cos(elev_low) - ti.cos(elev_high))
775
+
776
+ return d_omega