voxcity 0.7.0__py3-none-any.whl → 1.0.13__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.
- voxcity/__init__.py +14 -14
- voxcity/downloader/ocean.py +559 -0
- voxcity/exporter/__init__.py +12 -12
- voxcity/exporter/cityles.py +633 -633
- voxcity/exporter/envimet.py +733 -728
- voxcity/exporter/magicavoxel.py +333 -333
- voxcity/exporter/netcdf.py +238 -238
- voxcity/exporter/obj.py +1480 -1480
- voxcity/generator/__init__.py +47 -44
- voxcity/generator/api.py +727 -675
- voxcity/generator/grids.py +394 -379
- voxcity/generator/io.py +94 -94
- voxcity/generator/pipeline.py +582 -282
- voxcity/generator/update.py +429 -0
- voxcity/generator/voxelizer.py +18 -6
- voxcity/geoprocessor/__init__.py +75 -75
- voxcity/geoprocessor/draw.py +1494 -1219
- voxcity/geoprocessor/merge_utils.py +91 -91
- voxcity/geoprocessor/mesh.py +806 -806
- voxcity/geoprocessor/network.py +708 -708
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/buildings.py +435 -428
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/export.py +93 -93
- voxcity/geoprocessor/raster/landcover.py +178 -51
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/geoprocessor/utils.py +824 -824
- voxcity/models.py +115 -113
- voxcity/simulator/solar/__init__.py +66 -43
- voxcity/simulator/solar/integration.py +336 -336
- voxcity/simulator/solar/sky.py +668 -0
- voxcity/simulator/solar/temporal.py +792 -434
- voxcity/simulator_gpu/__init__.py +115 -0
- voxcity/simulator_gpu/common/__init__.py +9 -0
- voxcity/simulator_gpu/common/geometry.py +11 -0
- voxcity/simulator_gpu/core.py +322 -0
- voxcity/simulator_gpu/domain.py +262 -0
- voxcity/simulator_gpu/environment.yml +11 -0
- voxcity/simulator_gpu/init_taichi.py +154 -0
- voxcity/simulator_gpu/integration.py +15 -0
- voxcity/simulator_gpu/kernels.py +56 -0
- voxcity/simulator_gpu/radiation.py +28 -0
- voxcity/simulator_gpu/raytracing.py +623 -0
- voxcity/simulator_gpu/sky.py +9 -0
- voxcity/simulator_gpu/solar/__init__.py +178 -0
- voxcity/simulator_gpu/solar/core.py +66 -0
- voxcity/simulator_gpu/solar/csf.py +1249 -0
- voxcity/simulator_gpu/solar/domain.py +561 -0
- voxcity/simulator_gpu/solar/epw.py +421 -0
- voxcity/simulator_gpu/solar/integration.py +2953 -0
- voxcity/simulator_gpu/solar/radiation.py +3019 -0
- voxcity/simulator_gpu/solar/raytracing.py +686 -0
- voxcity/simulator_gpu/solar/reflection.py +533 -0
- voxcity/simulator_gpu/solar/sky.py +907 -0
- voxcity/simulator_gpu/solar/solar.py +337 -0
- voxcity/simulator_gpu/solar/svf.py +446 -0
- voxcity/simulator_gpu/solar/volumetric.py +1151 -0
- voxcity/simulator_gpu/solar/voxcity.py +2953 -0
- voxcity/simulator_gpu/temporal.py +13 -0
- voxcity/simulator_gpu/utils.py +25 -0
- voxcity/simulator_gpu/view.py +32 -0
- voxcity/simulator_gpu/visibility/__init__.py +109 -0
- voxcity/simulator_gpu/visibility/geometry.py +278 -0
- voxcity/simulator_gpu/visibility/integration.py +808 -0
- voxcity/simulator_gpu/visibility/landmark.py +753 -0
- voxcity/simulator_gpu/visibility/view.py +944 -0
- voxcity/utils/__init__.py +11 -0
- voxcity/utils/classes.py +194 -0
- voxcity/utils/lc.py +80 -39
- voxcity/utils/shape.py +230 -0
- voxcity/visualizer/__init__.py +24 -24
- voxcity/visualizer/builder.py +43 -43
- voxcity/visualizer/grids.py +141 -141
- voxcity/visualizer/maps.py +187 -187
- voxcity/visualizer/renderer.py +1146 -928
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/METADATA +56 -52
- voxcity-1.0.13.dist-info/RECORD +116 -0
- voxcity-0.7.0.dist-info/RECORD +0 -77
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-0.7.0.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,623 @@
|
|
|
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_trace_to_target(
|
|
471
|
+
origin: Vector3,
|
|
472
|
+
target: Vector3,
|
|
473
|
+
is_solid: ti.template(),
|
|
474
|
+
is_tree: 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
|
+
tree_att: ti.f32,
|
|
482
|
+
att_cutoff: ti.f32
|
|
483
|
+
):
|
|
484
|
+
"""
|
|
485
|
+
Trace ray from origin to target, checking for visibility.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
origin: Start position (in voxel coordinates)
|
|
489
|
+
target: End position (in voxel coordinates)
|
|
490
|
+
is_solid: 3D field of solid cells
|
|
491
|
+
is_tree: 3D field of tree cells
|
|
492
|
+
nx, ny, nz: Grid dimensions
|
|
493
|
+
dx, dy, dz: Cell sizes (typically all 1.0 for voxel coords)
|
|
494
|
+
tree_att: Attenuation factor per voxel for trees
|
|
495
|
+
att_cutoff: Minimum transmissivity before considering blocked
|
|
496
|
+
|
|
497
|
+
Returns:
|
|
498
|
+
1 if target is visible, 0 otherwise
|
|
499
|
+
"""
|
|
500
|
+
diff = target - origin
|
|
501
|
+
dist = diff.norm()
|
|
502
|
+
|
|
503
|
+
if dist < 0.01:
|
|
504
|
+
return 1
|
|
505
|
+
|
|
506
|
+
ray_dir = diff / dist
|
|
507
|
+
|
|
508
|
+
x, y, z = origin[0] + 0.5, origin[1] + 0.5, origin[2] + 0.5
|
|
509
|
+
i = ti.cast(ti.floor(origin[0]), ti.i32)
|
|
510
|
+
j = ti.cast(ti.floor(origin[1]), ti.i32)
|
|
511
|
+
k = ti.cast(ti.floor(origin[2]), ti.i32)
|
|
512
|
+
|
|
513
|
+
ti_x = ti.cast(ti.floor(target[0]), ti.i32)
|
|
514
|
+
tj_y = ti.cast(ti.floor(target[1]), ti.i32)
|
|
515
|
+
tk_z = ti.cast(ti.floor(target[2]), ti.i32)
|
|
516
|
+
|
|
517
|
+
step_x = 1 if ray_dir[0] >= 0 else -1
|
|
518
|
+
step_y = 1 if ray_dir[1] >= 0 else -1
|
|
519
|
+
step_z = 1 if ray_dir[2] >= 0 else -1
|
|
520
|
+
|
|
521
|
+
BIG = 1e30
|
|
522
|
+
t_max_x, t_max_y, t_max_z = BIG, BIG, BIG
|
|
523
|
+
t_delta_x, t_delta_y, t_delta_z = BIG, BIG, BIG
|
|
524
|
+
|
|
525
|
+
if ray_dir[0] != 0.0:
|
|
526
|
+
t_max_x = (((i + (1 if step_x > 0 else 0)) - x) / ray_dir[0])
|
|
527
|
+
t_delta_x = ti.abs(1.0 / ray_dir[0])
|
|
528
|
+
if ray_dir[1] != 0.0:
|
|
529
|
+
t_max_y = (((j + (1 if step_y > 0 else 0)) - y) / ray_dir[1])
|
|
530
|
+
t_delta_y = ti.abs(1.0 / ray_dir[1])
|
|
531
|
+
if ray_dir[2] != 0.0:
|
|
532
|
+
t_max_z = (((k + (1 if step_z > 0 else 0)) - z) / ray_dir[2])
|
|
533
|
+
t_delta_z = ti.abs(1.0 / ray_dir[2])
|
|
534
|
+
|
|
535
|
+
T = 1.0
|
|
536
|
+
visible = 1
|
|
537
|
+
max_steps = nx + ny + nz
|
|
538
|
+
done = 0
|
|
539
|
+
|
|
540
|
+
for _ in range(max_steps):
|
|
541
|
+
if done == 0:
|
|
542
|
+
if i < 0 or i >= nx or j < 0 or j >= ny or k < 0 or k >= nz:
|
|
543
|
+
visible = 0
|
|
544
|
+
done = 1
|
|
545
|
+
elif is_solid[i, j, k] == 1:
|
|
546
|
+
visible = 0
|
|
547
|
+
done = 1
|
|
548
|
+
elif is_tree[i, j, k] == 1:
|
|
549
|
+
T *= tree_att
|
|
550
|
+
if T < att_cutoff:
|
|
551
|
+
visible = 0
|
|
552
|
+
done = 1
|
|
553
|
+
|
|
554
|
+
if done == 0:
|
|
555
|
+
# Check if we've reached the target
|
|
556
|
+
if i == ti_x and j == tj_y and k == tk_z:
|
|
557
|
+
done = 1
|
|
558
|
+
else:
|
|
559
|
+
# Step to next voxel
|
|
560
|
+
if t_max_x < t_max_y:
|
|
561
|
+
if t_max_x < t_max_z:
|
|
562
|
+
t_max_x += t_delta_x
|
|
563
|
+
i += step_x
|
|
564
|
+
else:
|
|
565
|
+
t_max_z += t_delta_z
|
|
566
|
+
k += step_z
|
|
567
|
+
else:
|
|
568
|
+
if t_max_y < t_max_z:
|
|
569
|
+
t_max_y += t_delta_y
|
|
570
|
+
j += step_y
|
|
571
|
+
else:
|
|
572
|
+
t_max_z += t_delta_z
|
|
573
|
+
k += step_z
|
|
574
|
+
|
|
575
|
+
return visible
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
@ti.func
|
|
579
|
+
def sample_hemisphere_direction(i_azim: ti.i32, i_elev: ti.i32, n_azim: ti.i32, n_elev: ti.i32) -> Vector3:
|
|
580
|
+
"""
|
|
581
|
+
Generate a direction on the upper hemisphere.
|
|
582
|
+
|
|
583
|
+
Args:
|
|
584
|
+
i_azim: Azimuthal index (0 to n_azim-1)
|
|
585
|
+
i_elev: Elevation index (0 to n_elev-1)
|
|
586
|
+
n_azim: Number of azimuthal divisions
|
|
587
|
+
n_elev: Number of elevation divisions
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
Unit direction vector
|
|
591
|
+
"""
|
|
592
|
+
PI = 3.14159265359
|
|
593
|
+
|
|
594
|
+
# Elevation angle (from zenith)
|
|
595
|
+
elev = (i_elev + 0.5) * (PI / 2.0) / n_elev
|
|
596
|
+
|
|
597
|
+
# Azimuth angle
|
|
598
|
+
azim = (i_azim + 0.5) * (2.0 * PI) / n_azim
|
|
599
|
+
|
|
600
|
+
# Convert to Cartesian (z up)
|
|
601
|
+
sin_elev = ti.sin(elev)
|
|
602
|
+
cos_elev = ti.cos(elev)
|
|
603
|
+
|
|
604
|
+
x = sin_elev * ti.sin(azim)
|
|
605
|
+
y = sin_elev * ti.cos(azim)
|
|
606
|
+
z = cos_elev
|
|
607
|
+
|
|
608
|
+
return Vector3(x, y, z)
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@ti.func
|
|
612
|
+
def hemisphere_solid_angle(i_elev: ti.i32, n_azim: ti.i32, n_elev: ti.i32) -> ti.f32:
|
|
613
|
+
"""
|
|
614
|
+
Calculate solid angle for a hemisphere segment.
|
|
615
|
+
"""
|
|
616
|
+
PI = 3.14159265359
|
|
617
|
+
|
|
618
|
+
elev_low = i_elev * (PI / 2.0) / n_elev
|
|
619
|
+
elev_high = (i_elev + 1) * (PI / 2.0) / n_elev
|
|
620
|
+
|
|
621
|
+
d_omega = (2.0 * PI / n_azim) * (ti.cos(elev_low) - ti.cos(elev_high))
|
|
622
|
+
|
|
623
|
+
return d_omega
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""VoxCity-style `sky` module (toplevel) for compatibility.
|
|
2
|
+
|
|
3
|
+
VoxCity exposes sky patch utilities under `voxcity.simulator.solar.sky`, but the
|
|
4
|
+
flattened `voxcity.simulator` namespace often ends up with a `sky` attribute.
|
|
5
|
+
|
|
6
|
+
This module forwards to `simulator_gpu.solar.sky`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .solar.sky import * # noqa: F401,F403
|