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,944 @@
|
|
|
1
|
+
"""
|
|
2
|
+
View Index and Sky View Factor calculation using Taichi GPU acceleration.
|
|
3
|
+
|
|
4
|
+
This module emulates the functionality of voxcity.simulator.visibility.view
|
|
5
|
+
with GPU-accelerated ray tracing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import taichi as ti
|
|
9
|
+
import numpy as np
|
|
10
|
+
import math
|
|
11
|
+
from typing import Tuple, Optional, Union, List
|
|
12
|
+
|
|
13
|
+
from ..core import Vector3, Point3, PI, TWO_PI
|
|
14
|
+
from ..init_taichi import ensure_initialized
|
|
15
|
+
from ..raytracing import (
|
|
16
|
+
ray_voxel_first_hit,
|
|
17
|
+
ray_voxel_transmissivity,
|
|
18
|
+
ray_aabb_intersect,
|
|
19
|
+
)
|
|
20
|
+
from .geometry import (
|
|
21
|
+
generate_ray_directions_grid,
|
|
22
|
+
generate_ray_directions_fibonacci,
|
|
23
|
+
generate_hemisphere_directions,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@ti.data_oriented
|
|
28
|
+
class ViewCalculator:
|
|
29
|
+
"""
|
|
30
|
+
GPU-accelerated View Index calculator.
|
|
31
|
+
|
|
32
|
+
Computes view indices (green view, sky view, custom targets) by tracing rays
|
|
33
|
+
from observer positions through the voxel domain.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
domain,
|
|
39
|
+
n_azimuth: int = 120,
|
|
40
|
+
n_elevation: int = 20,
|
|
41
|
+
ray_sampling: str = "grid",
|
|
42
|
+
n_rays: int = None
|
|
43
|
+
):
|
|
44
|
+
"""
|
|
45
|
+
Initialize View Calculator.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
domain: Domain object with grid geometry (simulator_gpu.domain.Domain)
|
|
49
|
+
n_azimuth: Number of azimuthal divisions (for grid sampling)
|
|
50
|
+
n_elevation: Number of elevation divisions (for grid sampling)
|
|
51
|
+
ray_sampling: Sampling method ('grid' or 'fibonacci')
|
|
52
|
+
n_rays: Total rays for fibonacci sampling (default: n_azimuth * n_elevation)
|
|
53
|
+
"""
|
|
54
|
+
# Ensure Taichi is initialized before creating any fields
|
|
55
|
+
ensure_initialized()
|
|
56
|
+
|
|
57
|
+
self.domain = domain
|
|
58
|
+
self.nx = domain.nx
|
|
59
|
+
self.ny = domain.ny
|
|
60
|
+
self.nz = domain.nz
|
|
61
|
+
self.dx = domain.dx
|
|
62
|
+
self.dy = domain.dy
|
|
63
|
+
self.dz = domain.dz
|
|
64
|
+
|
|
65
|
+
self.n_azimuth = n_azimuth
|
|
66
|
+
self.n_elevation = n_elevation
|
|
67
|
+
self.ray_sampling = ray_sampling
|
|
68
|
+
self.n_rays = n_rays if n_rays else n_azimuth * n_elevation
|
|
69
|
+
|
|
70
|
+
# Maximum ray distance
|
|
71
|
+
self.max_dist = domain.get_max_dist()
|
|
72
|
+
|
|
73
|
+
# Pre-computed ray directions (will be set based on mode)
|
|
74
|
+
self._ray_dirs = None
|
|
75
|
+
self._n_ray_dirs = 0
|
|
76
|
+
|
|
77
|
+
def _setup_ray_directions(
|
|
78
|
+
self,
|
|
79
|
+
elevation_min: float = -30.0,
|
|
80
|
+
elevation_max: float = 30.0
|
|
81
|
+
):
|
|
82
|
+
"""Setup ray directions based on sampling method."""
|
|
83
|
+
if self.ray_sampling.lower() == "fibonacci":
|
|
84
|
+
dirs_np = generate_ray_directions_fibonacci(
|
|
85
|
+
self.n_rays, elevation_min, elevation_max
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
dirs_np = generate_ray_directions_grid(
|
|
89
|
+
self.n_azimuth, self.n_elevation, elevation_min, elevation_max
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
self._n_ray_dirs = dirs_np.shape[0]
|
|
93
|
+
self._ray_dirs = ti.Vector.field(3, dtype=ti.f32, shape=(self._n_ray_dirs,))
|
|
94
|
+
self._ray_dirs.from_numpy(dirs_np)
|
|
95
|
+
|
|
96
|
+
def compute_view_index(
|
|
97
|
+
self,
|
|
98
|
+
voxel_data: np.ndarray = None,
|
|
99
|
+
mode: str = None,
|
|
100
|
+
hit_values: Tuple[int, ...] = None,
|
|
101
|
+
inclusion_mode: bool = True,
|
|
102
|
+
view_point_height: float = 1.5,
|
|
103
|
+
elevation_min_degrees: float = -30.0,
|
|
104
|
+
elevation_max_degrees: float = 30.0,
|
|
105
|
+
tree_k: float = 0.5,
|
|
106
|
+
tree_lad: float = 1.0
|
|
107
|
+
) -> np.ndarray:
|
|
108
|
+
"""
|
|
109
|
+
Compute View Index map.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
voxel_data: 3D voxel class array (optional if domain has voxel data)
|
|
113
|
+
mode: Predefined mode ('green', 'sky', or None for custom)
|
|
114
|
+
hit_values: Target voxel values to count as visible
|
|
115
|
+
inclusion_mode: If True, count hits on targets; if False, count non-blocked rays
|
|
116
|
+
view_point_height: Observer height above ground (meters)
|
|
117
|
+
elevation_min_degrees: Minimum viewing angle
|
|
118
|
+
elevation_max_degrees: Maximum viewing angle
|
|
119
|
+
tree_k: Tree extinction coefficient
|
|
120
|
+
tree_lad: Leaf area density for trees
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
2D array of view index values (nx, ny)
|
|
124
|
+
"""
|
|
125
|
+
# Set up mode-specific parameters
|
|
126
|
+
if mode == 'green':
|
|
127
|
+
hit_values = (-2, 2, 5, 6, 7, 8)
|
|
128
|
+
inclusion_mode = True
|
|
129
|
+
elif mode == 'sky':
|
|
130
|
+
hit_values = (0,)
|
|
131
|
+
inclusion_mode = False
|
|
132
|
+
elif hit_values is None:
|
|
133
|
+
raise ValueError("For custom mode, you must provide hit_values.")
|
|
134
|
+
|
|
135
|
+
# Setup ray directions
|
|
136
|
+
self._setup_ray_directions(elevation_min_degrees, elevation_max_degrees)
|
|
137
|
+
|
|
138
|
+
# Convert view height to voxels
|
|
139
|
+
view_height_voxel = int(view_point_height / self.dz)
|
|
140
|
+
|
|
141
|
+
# Prepare output
|
|
142
|
+
vi_map = ti.field(dtype=ti.f32, shape=(self.nx, self.ny))
|
|
143
|
+
|
|
144
|
+
# Prepare masks from voxel data
|
|
145
|
+
if voxel_data is None:
|
|
146
|
+
# Use domain's is_solid and is_tree
|
|
147
|
+
is_tree = self.domain.is_tree
|
|
148
|
+
is_solid = self.domain.is_solid
|
|
149
|
+
is_target = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
150
|
+
is_allowed = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
151
|
+
is_blocker = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
152
|
+
is_walkable = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
153
|
+
|
|
154
|
+
# Initialize target/allowed based on mode
|
|
155
|
+
self._init_target_masks_from_domain(
|
|
156
|
+
is_tree, is_solid, is_target, is_allowed, is_blocker, is_walkable,
|
|
157
|
+
inclusion_mode, mode == 'green'
|
|
158
|
+
)
|
|
159
|
+
else:
|
|
160
|
+
# Create masks from voxel_data
|
|
161
|
+
is_tree = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
162
|
+
is_solid = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
163
|
+
is_target = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
164
|
+
is_allowed = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
165
|
+
is_blocker = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
166
|
+
is_walkable = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
167
|
+
|
|
168
|
+
# Convert hit_values to array for kernel
|
|
169
|
+
hit_values_arr = np.array(hit_values, dtype=np.int32)
|
|
170
|
+
|
|
171
|
+
self._setup_masks_from_voxel_data(
|
|
172
|
+
voxel_data, hit_values_arr, inclusion_mode,
|
|
173
|
+
is_tree, is_solid, is_target, is_allowed, is_blocker, is_walkable
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Compute transmissivity per voxel for trees
|
|
177
|
+
tree_att = float(math.exp(-tree_k * tree_lad * self.dz))
|
|
178
|
+
|
|
179
|
+
# Run GPU computation
|
|
180
|
+
self._compute_vi_map_kernel(
|
|
181
|
+
vi_map, view_height_voxel,
|
|
182
|
+
is_tree, is_solid, is_target, is_allowed, is_blocker, is_walkable,
|
|
183
|
+
inclusion_mode, tree_att
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Flip Y-axis to match VoxCity coordinate system
|
|
187
|
+
result = np.flipud(vi_map.to_numpy())
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
@ti.kernel
|
|
191
|
+
def _init_target_masks_from_domain(
|
|
192
|
+
self,
|
|
193
|
+
is_tree: ti.template(),
|
|
194
|
+
is_solid: ti.template(),
|
|
195
|
+
is_target: ti.template(),
|
|
196
|
+
is_allowed: ti.template(),
|
|
197
|
+
is_blocker: ti.template(),
|
|
198
|
+
is_walkable: ti.template(),
|
|
199
|
+
inclusion_mode: ti.i32,
|
|
200
|
+
green_mode: ti.i32
|
|
201
|
+
):
|
|
202
|
+
"""Initialize target masks from domain's is_tree and is_solid."""
|
|
203
|
+
for i, j, k in is_target:
|
|
204
|
+
# Without voxel_data, assume all surfaces are walkable
|
|
205
|
+
is_walkable[i, j, k] = 1
|
|
206
|
+
|
|
207
|
+
if green_mode == 1:
|
|
208
|
+
# Green mode: trees are targets
|
|
209
|
+
is_target[i, j, k] = is_tree[i, j, k]
|
|
210
|
+
is_allowed[i, j, k] = is_tree[i, j, k]
|
|
211
|
+
# Blockers are solids (non-tree)
|
|
212
|
+
blocker = 0
|
|
213
|
+
if is_solid[i, j, k] == 1 and is_tree[i, j, k] == 0:
|
|
214
|
+
blocker = 1
|
|
215
|
+
is_blocker[i, j, k] = blocker
|
|
216
|
+
else:
|
|
217
|
+
# Sky mode: no targets, just check for blockers
|
|
218
|
+
is_target[i, j, k] = 0
|
|
219
|
+
is_allowed[i, j, k] = 0
|
|
220
|
+
is_blocker[i, j, k] = 0
|
|
221
|
+
|
|
222
|
+
def _setup_masks_from_voxel_data(
|
|
223
|
+
self,
|
|
224
|
+
voxel_data: np.ndarray,
|
|
225
|
+
hit_values: np.ndarray,
|
|
226
|
+
inclusion_mode: bool,
|
|
227
|
+
is_tree: ti.template(),
|
|
228
|
+
is_solid: ti.template(),
|
|
229
|
+
is_target: ti.template(),
|
|
230
|
+
is_allowed: ti.template(),
|
|
231
|
+
is_blocker: ti.template(),
|
|
232
|
+
is_walkable: ti.template()
|
|
233
|
+
):
|
|
234
|
+
"""Setup masks from voxel data array."""
|
|
235
|
+
n_hits = len(hit_values)
|
|
236
|
+
self._setup_masks_kernel(
|
|
237
|
+
voxel_data, hit_values, n_hits, inclusion_mode,
|
|
238
|
+
is_tree, is_solid, is_target, is_allowed, is_blocker, is_walkable
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
@ti.kernel
|
|
242
|
+
def _setup_masks_kernel(
|
|
243
|
+
self,
|
|
244
|
+
voxel_data: ti.types.ndarray(),
|
|
245
|
+
hit_values: ti.types.ndarray(),
|
|
246
|
+
n_hits: ti.i32,
|
|
247
|
+
inclusion_mode: ti.i32,
|
|
248
|
+
is_tree: ti.template(),
|
|
249
|
+
is_solid: ti.template(),
|
|
250
|
+
is_target: ti.template(),
|
|
251
|
+
is_allowed: ti.template(),
|
|
252
|
+
is_blocker: ti.template(),
|
|
253
|
+
is_walkable: ti.template()
|
|
254
|
+
):
|
|
255
|
+
for i, j, k in is_tree:
|
|
256
|
+
val = voxel_data[i, j, k]
|
|
257
|
+
|
|
258
|
+
# Tree check (code -2)
|
|
259
|
+
tree = 0
|
|
260
|
+
if val == -2:
|
|
261
|
+
tree = 1
|
|
262
|
+
is_tree[i, j, k] = tree
|
|
263
|
+
|
|
264
|
+
# Solid check (non-zero, non-tree)
|
|
265
|
+
solid = 0
|
|
266
|
+
if val != 0 and val != -2:
|
|
267
|
+
solid = 1
|
|
268
|
+
is_solid[i, j, k] = solid
|
|
269
|
+
|
|
270
|
+
# Target check
|
|
271
|
+
target = 0
|
|
272
|
+
for h in range(n_hits):
|
|
273
|
+
if val == hit_values[h]:
|
|
274
|
+
target = 1
|
|
275
|
+
is_target[i, j, k] = target
|
|
276
|
+
|
|
277
|
+
# Walkable: surfaces that are valid observer positions
|
|
278
|
+
# Exclude: water (7, 8, 9) and negative values (ground -1, tree -2, building -3, etc.)
|
|
279
|
+
walkable = 1
|
|
280
|
+
if val == 7 or val == 8 or val == 9: # Water
|
|
281
|
+
walkable = 0
|
|
282
|
+
elif val < 0: # Ground, trees, buildings, landmarks, etc.
|
|
283
|
+
walkable = 0
|
|
284
|
+
is_walkable[i, j, k] = walkable
|
|
285
|
+
|
|
286
|
+
# Set up allowed and blocker based on mode
|
|
287
|
+
if inclusion_mode == 1:
|
|
288
|
+
is_allowed[i, j, k] = target
|
|
289
|
+
# Blocker: non-tree, non-target, non-empty
|
|
290
|
+
blocker = 0
|
|
291
|
+
if val != 0 and tree == 0 and target == 0:
|
|
292
|
+
blocker = 1
|
|
293
|
+
is_blocker[i, j, k] = blocker
|
|
294
|
+
else:
|
|
295
|
+
is_allowed[i, j, k] = target
|
|
296
|
+
is_blocker[i, j, k] = 0
|
|
297
|
+
|
|
298
|
+
@ti.kernel
|
|
299
|
+
def _compute_vi_map_kernel(
|
|
300
|
+
self,
|
|
301
|
+
vi_map: ti.template(),
|
|
302
|
+
view_height_voxel: ti.i32,
|
|
303
|
+
is_tree: ti.template(),
|
|
304
|
+
is_solid: ti.template(),
|
|
305
|
+
is_target: ti.template(),
|
|
306
|
+
is_allowed: ti.template(),
|
|
307
|
+
is_blocker: ti.template(),
|
|
308
|
+
is_walkable: ti.template(),
|
|
309
|
+
inclusion_mode: ti.i32,
|
|
310
|
+
tree_att: ti.f32
|
|
311
|
+
):
|
|
312
|
+
"""Compute View Index map using GPU parallel processing."""
|
|
313
|
+
for x, y in vi_map:
|
|
314
|
+
# Find observer position (first non-solid cell above ground)
|
|
315
|
+
# Allow being inside tree canopy
|
|
316
|
+
observer_z = -1
|
|
317
|
+
surface_walkable = 0
|
|
318
|
+
for z in range(1, self.nz):
|
|
319
|
+
# Current cell is not solid (but can be inside tree)
|
|
320
|
+
current_not_solid = 1 if is_solid[x, y, z] == 0 else 0
|
|
321
|
+
# Below cell has something (ground, building, or tree)
|
|
322
|
+
below_has_something = is_solid[x, y, z-1] + is_tree[x, y, z-1]
|
|
323
|
+
|
|
324
|
+
if current_not_solid == 1 and below_has_something > 0:
|
|
325
|
+
# Found ground level - check if walkable
|
|
326
|
+
surface_walkable = is_walkable[x, y, z-1]
|
|
327
|
+
observer_z = z + view_height_voxel
|
|
328
|
+
break
|
|
329
|
+
|
|
330
|
+
# Mark as invalid if no observer position found or surface not walkable
|
|
331
|
+
# (water, building tops, etc. are not walkable)
|
|
332
|
+
if observer_z < 0 or observer_z >= self.nz or surface_walkable == 0:
|
|
333
|
+
vi_map[x, y] = ti.cast(float('nan'), ti.f32)
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
# Trace rays and count visibility
|
|
337
|
+
visibility_sum = 0.0
|
|
338
|
+
valid_rays = 0
|
|
339
|
+
|
|
340
|
+
for r in range(self._n_ray_dirs):
|
|
341
|
+
ray_dir = self._ray_dirs[r]
|
|
342
|
+
|
|
343
|
+
# Trace ray through voxels
|
|
344
|
+
hit, trans = self._trace_ray_vi(
|
|
345
|
+
x, y, observer_z, ray_dir,
|
|
346
|
+
is_tree, is_solid, is_target, is_allowed, is_blocker,
|
|
347
|
+
inclusion_mode, tree_att
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if inclusion_mode == 1:
|
|
351
|
+
if hit == 1:
|
|
352
|
+
visibility_sum += 1.0
|
|
353
|
+
else:
|
|
354
|
+
if hit == 0:
|
|
355
|
+
visibility_sum += trans
|
|
356
|
+
|
|
357
|
+
valid_rays += 1
|
|
358
|
+
|
|
359
|
+
if valid_rays > 0:
|
|
360
|
+
vi_map[x, y] = visibility_sum / valid_rays
|
|
361
|
+
else:
|
|
362
|
+
vi_map[x, y] = 0.0
|
|
363
|
+
|
|
364
|
+
@ti.func
|
|
365
|
+
def _trace_ray_vi(
|
|
366
|
+
self,
|
|
367
|
+
ox: ti.i32,
|
|
368
|
+
oy: ti.i32,
|
|
369
|
+
oz: ti.i32,
|
|
370
|
+
ray_dir: Vector3,
|
|
371
|
+
is_tree: ti.template(),
|
|
372
|
+
is_solid: ti.template(),
|
|
373
|
+
is_target: ti.template(),
|
|
374
|
+
is_allowed: ti.template(),
|
|
375
|
+
is_blocker: ti.template(),
|
|
376
|
+
inclusion_mode: ti.i32,
|
|
377
|
+
tree_att: ti.f32
|
|
378
|
+
):
|
|
379
|
+
"""
|
|
380
|
+
Trace a ray for view index calculation.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
(hit, transmissivity) where hit=1 means target was hit (inclusion)
|
|
384
|
+
or ray was blocked (exclusion)
|
|
385
|
+
"""
|
|
386
|
+
hit = 0
|
|
387
|
+
trans = 1.0
|
|
388
|
+
|
|
389
|
+
# Start from observer position
|
|
390
|
+
x = ti.cast(ox, ti.f32) + 0.5
|
|
391
|
+
y = ti.cast(oy, ti.f32) + 0.5
|
|
392
|
+
z = ti.cast(oz, ti.f32) + 0.5
|
|
393
|
+
|
|
394
|
+
i = ox
|
|
395
|
+
j = oy
|
|
396
|
+
k = oz
|
|
397
|
+
|
|
398
|
+
# Track starting position to skip it
|
|
399
|
+
start_i = ox
|
|
400
|
+
start_j = oy
|
|
401
|
+
start_k = oz
|
|
402
|
+
|
|
403
|
+
step_x = 1 if ray_dir[0] >= 0 else -1
|
|
404
|
+
step_y = 1 if ray_dir[1] >= 0 else -1
|
|
405
|
+
step_z = 1 if ray_dir[2] >= 0 else -1
|
|
406
|
+
|
|
407
|
+
BIG = 1e30
|
|
408
|
+
t_max_x, t_max_y, t_max_z = BIG, BIG, BIG
|
|
409
|
+
t_delta_x, t_delta_y, t_delta_z = BIG, BIG, BIG
|
|
410
|
+
|
|
411
|
+
if ti.abs(ray_dir[0]) > 1e-10:
|
|
412
|
+
t_max_x = ((i + (1 if step_x > 0 else 0)) - x) / ray_dir[0]
|
|
413
|
+
t_delta_x = ti.abs(1.0 / ray_dir[0])
|
|
414
|
+
if ti.abs(ray_dir[1]) > 1e-10:
|
|
415
|
+
t_max_y = ((j + (1 if step_y > 0 else 0)) - y) / ray_dir[1]
|
|
416
|
+
t_delta_y = ti.abs(1.0 / ray_dir[1])
|
|
417
|
+
if ti.abs(ray_dir[2]) > 1e-10:
|
|
418
|
+
t_max_z = ((k + (1 if step_z > 0 else 0)) - z) / ray_dir[2]
|
|
419
|
+
t_delta_z = ti.abs(1.0 / ray_dir[2])
|
|
420
|
+
|
|
421
|
+
max_steps = self.nx + self.ny + self.nz
|
|
422
|
+
done = 0
|
|
423
|
+
first_step = 1 # Skip the starting voxel
|
|
424
|
+
|
|
425
|
+
for _ in range(max_steps):
|
|
426
|
+
if done == 0:
|
|
427
|
+
# Bounds check
|
|
428
|
+
if i < 0 or i >= self.nx or j < 0 or j >= self.ny or k < 0 or k >= self.nz:
|
|
429
|
+
done = 1
|
|
430
|
+
else:
|
|
431
|
+
# Skip the starting voxel (observer's position)
|
|
432
|
+
at_start = 0
|
|
433
|
+
if i == start_i and j == start_j and k == start_k:
|
|
434
|
+
at_start = 1
|
|
435
|
+
|
|
436
|
+
if at_start == 0:
|
|
437
|
+
# Check tree - accumulate transmissivity
|
|
438
|
+
if is_tree[i, j, k] == 1:
|
|
439
|
+
trans *= tree_att
|
|
440
|
+
if trans < 0.01:
|
|
441
|
+
if inclusion_mode == 0:
|
|
442
|
+
hit = 1 # Blocked in exclusion mode
|
|
443
|
+
done = 1
|
|
444
|
+
|
|
445
|
+
if done == 0:
|
|
446
|
+
if inclusion_mode == 1:
|
|
447
|
+
# Inclusion mode: looking for target (including trees as targets)
|
|
448
|
+
if is_target[i, j, k] == 1:
|
|
449
|
+
hit = 1
|
|
450
|
+
done = 1
|
|
451
|
+
elif is_blocker[i, j, k] == 1:
|
|
452
|
+
hit = 0
|
|
453
|
+
done = 1
|
|
454
|
+
else:
|
|
455
|
+
# Exclusion mode: looking for non-allowed
|
|
456
|
+
if is_tree[i, j, k] == 0 and is_allowed[i, j, k] == 0 and is_solid[i, j, k] == 1:
|
|
457
|
+
hit = 1 # Blocked
|
|
458
|
+
done = 1
|
|
459
|
+
|
|
460
|
+
if done == 0:
|
|
461
|
+
# Step to next voxel
|
|
462
|
+
if t_max_x < t_max_y:
|
|
463
|
+
if t_max_x < t_max_z:
|
|
464
|
+
t_max_x += t_delta_x
|
|
465
|
+
i += step_x
|
|
466
|
+
else:
|
|
467
|
+
t_max_z += t_delta_z
|
|
468
|
+
k += step_z
|
|
469
|
+
else:
|
|
470
|
+
if t_max_y < t_max_z:
|
|
471
|
+
t_max_y += t_delta_y
|
|
472
|
+
j += step_y
|
|
473
|
+
else:
|
|
474
|
+
t_max_z += t_delta_z
|
|
475
|
+
k += step_z
|
|
476
|
+
|
|
477
|
+
return hit, trans
|
|
478
|
+
|
|
479
|
+
def compute_sky_view_factor(
|
|
480
|
+
self,
|
|
481
|
+
voxel_data: np.ndarray = None,
|
|
482
|
+
view_point_height: float = 1.5,
|
|
483
|
+
n_azimuth: int = 120,
|
|
484
|
+
n_elevation: int = 20,
|
|
485
|
+
tree_k: float = 0.6,
|
|
486
|
+
tree_lad: float = 1.0
|
|
487
|
+
) -> np.ndarray:
|
|
488
|
+
"""
|
|
489
|
+
Compute Sky View Factor map.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
voxel_data: 3D voxel class array (optional)
|
|
493
|
+
view_point_height: Observer height above ground (meters)
|
|
494
|
+
n_azimuth: Number of azimuthal divisions
|
|
495
|
+
n_elevation: Number of elevation divisions
|
|
496
|
+
tree_k: Tree extinction coefficient
|
|
497
|
+
tree_lad: Leaf area density
|
|
498
|
+
|
|
499
|
+
Returns:
|
|
500
|
+
2D array of SVF values (nx, ny)
|
|
501
|
+
"""
|
|
502
|
+
# SVF uses upper hemisphere (0-90 degrees elevation)
|
|
503
|
+
self.n_azimuth = n_azimuth
|
|
504
|
+
self.n_elevation = n_elevation
|
|
505
|
+
|
|
506
|
+
return self.compute_view_index(
|
|
507
|
+
voxel_data=voxel_data,
|
|
508
|
+
mode='sky',
|
|
509
|
+
view_point_height=view_point_height,
|
|
510
|
+
elevation_min_degrees=0.0,
|
|
511
|
+
elevation_max_degrees=90.0,
|
|
512
|
+
tree_k=tree_k,
|
|
513
|
+
tree_lad=tree_lad
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
@ti.data_oriented
|
|
518
|
+
class SurfaceViewFactorCalculator:
|
|
519
|
+
"""
|
|
520
|
+
GPU-accelerated Surface View Factor calculator.
|
|
521
|
+
|
|
522
|
+
Computes view factors for building surface faces by tracing rays from
|
|
523
|
+
face centers through the voxel domain.
|
|
524
|
+
|
|
525
|
+
This emulates voxcity.simulator.visibility.view.get_surface_view_factor
|
|
526
|
+
using Taichi GPU acceleration.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
def __init__(
|
|
530
|
+
self,
|
|
531
|
+
domain,
|
|
532
|
+
n_azimuth: int = 120,
|
|
533
|
+
n_elevation: int = 20,
|
|
534
|
+
ray_sampling: str = "grid",
|
|
535
|
+
n_rays: int = None
|
|
536
|
+
):
|
|
537
|
+
"""
|
|
538
|
+
Initialize Surface View Factor Calculator.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
domain: Domain object with grid geometry
|
|
542
|
+
n_azimuth: Number of azimuthal divisions
|
|
543
|
+
n_elevation: Number of elevation divisions
|
|
544
|
+
ray_sampling: 'grid' or 'fibonacci'
|
|
545
|
+
n_rays: Total rays for fibonacci sampling
|
|
546
|
+
"""
|
|
547
|
+
self.domain = domain
|
|
548
|
+
self.nx = domain.nx
|
|
549
|
+
self.ny = domain.ny
|
|
550
|
+
self.nz = domain.nz
|
|
551
|
+
self.dx = domain.dx
|
|
552
|
+
self.dy = domain.dy
|
|
553
|
+
self.dz = domain.dz
|
|
554
|
+
self.meshsize = domain.dx # Assuming uniform grid
|
|
555
|
+
|
|
556
|
+
self.n_azimuth = n_azimuth
|
|
557
|
+
self.n_elevation = n_elevation
|
|
558
|
+
self.ray_sampling = ray_sampling
|
|
559
|
+
self.n_rays = n_rays if n_rays else n_azimuth * n_elevation
|
|
560
|
+
|
|
561
|
+
# Pre-computed hemisphere directions (upper hemisphere only for surfaces)
|
|
562
|
+
self._hemisphere_dirs = None
|
|
563
|
+
self._n_hemisphere_dirs = 0
|
|
564
|
+
self._setup_hemisphere_directions()
|
|
565
|
+
|
|
566
|
+
def _setup_hemisphere_directions(self):
|
|
567
|
+
"""Setup hemisphere ray directions for surface view factor."""
|
|
568
|
+
if self.ray_sampling.lower() == "fibonacci":
|
|
569
|
+
dirs_np = generate_ray_directions_fibonacci(
|
|
570
|
+
self.n_rays, 0.0, 90.0
|
|
571
|
+
)
|
|
572
|
+
else:
|
|
573
|
+
dirs_np = generate_ray_directions_grid(
|
|
574
|
+
self.n_azimuth, self.n_elevation, 0.0, 90.0
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
self._n_hemisphere_dirs = dirs_np.shape[0]
|
|
578
|
+
self._hemisphere_dirs = ti.Vector.field(3, dtype=ti.f32, shape=(self._n_hemisphere_dirs,))
|
|
579
|
+
self._hemisphere_dirs.from_numpy(dirs_np.astype(np.float32))
|
|
580
|
+
|
|
581
|
+
def compute_surface_view_factor(
|
|
582
|
+
self,
|
|
583
|
+
face_centers: np.ndarray,
|
|
584
|
+
face_normals: np.ndarray,
|
|
585
|
+
voxel_data: np.ndarray = None,
|
|
586
|
+
target_values: Tuple[int, ...] = (0,),
|
|
587
|
+
inclusion_mode: bool = False,
|
|
588
|
+
tree_k: float = 0.6,
|
|
589
|
+
tree_lad: float = 1.0,
|
|
590
|
+
boundary_epsilon: float = None
|
|
591
|
+
) -> np.ndarray:
|
|
592
|
+
"""
|
|
593
|
+
Compute view factors for building surface faces.
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
face_centers: Array of face center positions (n_faces, 3) in world coords
|
|
597
|
+
face_normals: Array of face normal vectors (n_faces, 3)
|
|
598
|
+
voxel_data: 3D voxel class array
|
|
599
|
+
target_values: Target voxel values for visibility
|
|
600
|
+
inclusion_mode: If True, count hits on targets; if False, count unblocked rays
|
|
601
|
+
tree_k: Tree extinction coefficient
|
|
602
|
+
tree_lad: Leaf area density
|
|
603
|
+
boundary_epsilon: Epsilon for boundary detection
|
|
604
|
+
|
|
605
|
+
Returns:
|
|
606
|
+
1D array of view factor values for each face
|
|
607
|
+
"""
|
|
608
|
+
n_faces = face_centers.shape[0]
|
|
609
|
+
|
|
610
|
+
if boundary_epsilon is None:
|
|
611
|
+
boundary_epsilon = self.meshsize * 0.05
|
|
612
|
+
|
|
613
|
+
# Grid bounds in world coordinates
|
|
614
|
+
grid_bounds_real = np.array([
|
|
615
|
+
[0.0, 0.0, 0.0],
|
|
616
|
+
[self.nx * self.meshsize, self.ny * self.meshsize, self.nz * self.meshsize]
|
|
617
|
+
], dtype=np.float32)
|
|
618
|
+
|
|
619
|
+
# Prepare Taichi fields
|
|
620
|
+
face_centers_ti = ti.Vector.field(3, dtype=ti.f32, shape=(n_faces,))
|
|
621
|
+
face_normals_ti = ti.Vector.field(3, dtype=ti.f32, shape=(n_faces,))
|
|
622
|
+
face_vf_values = ti.field(dtype=ti.f32, shape=(n_faces,))
|
|
623
|
+
|
|
624
|
+
face_centers_ti.from_numpy(face_centers.astype(np.float32))
|
|
625
|
+
face_normals_ti.from_numpy(face_normals.astype(np.float32))
|
|
626
|
+
|
|
627
|
+
# Prepare masks
|
|
628
|
+
if voxel_data is not None:
|
|
629
|
+
is_tree = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
630
|
+
is_solid = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
631
|
+
is_target = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
632
|
+
is_opaque = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
633
|
+
|
|
634
|
+
target_values_arr = np.array(target_values, dtype=np.int32)
|
|
635
|
+
self._setup_surface_masks(
|
|
636
|
+
voxel_data, target_values_arr, len(target_values_arr), inclusion_mode,
|
|
637
|
+
is_tree, is_solid, is_target, is_opaque
|
|
638
|
+
)
|
|
639
|
+
else:
|
|
640
|
+
is_tree = self.domain.is_tree
|
|
641
|
+
is_solid = self.domain.is_solid
|
|
642
|
+
is_target = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
643
|
+
is_opaque = ti.field(dtype=ti.i32, shape=(self.nx, self.ny, self.nz))
|
|
644
|
+
|
|
645
|
+
# Tree attenuation
|
|
646
|
+
tree_att = float(math.exp(-tree_k * tree_lad * self.meshsize))
|
|
647
|
+
att_cutoff = 0.01
|
|
648
|
+
trees_are_targets = (-2 in target_values) and inclusion_mode
|
|
649
|
+
|
|
650
|
+
# Run GPU computation
|
|
651
|
+
self._compute_surface_vf_kernel(
|
|
652
|
+
face_centers_ti, face_normals_ti, face_vf_values,
|
|
653
|
+
is_tree, is_solid, is_target, is_opaque,
|
|
654
|
+
tree_att, att_cutoff, inclusion_mode, int(trees_are_targets),
|
|
655
|
+
grid_bounds_real, boundary_epsilon
|
|
656
|
+
)
|
|
657
|
+
|
|
658
|
+
return face_vf_values.to_numpy()
|
|
659
|
+
|
|
660
|
+
@ti.kernel
|
|
661
|
+
def _setup_surface_masks(
|
|
662
|
+
self,
|
|
663
|
+
voxel_data: ti.types.ndarray(),
|
|
664
|
+
target_values: ti.types.ndarray(),
|
|
665
|
+
n_targets: ti.i32,
|
|
666
|
+
inclusion_mode: ti.i32,
|
|
667
|
+
is_tree: ti.template(),
|
|
668
|
+
is_solid: ti.template(),
|
|
669
|
+
is_target: ti.template(),
|
|
670
|
+
is_opaque: ti.template()
|
|
671
|
+
):
|
|
672
|
+
for i, j, k in is_tree:
|
|
673
|
+
val = voxel_data[i, j, k]
|
|
674
|
+
|
|
675
|
+
# Tree check
|
|
676
|
+
tree = 0
|
|
677
|
+
if val == -2:
|
|
678
|
+
tree = 1
|
|
679
|
+
is_tree[i, j, k] = tree
|
|
680
|
+
|
|
681
|
+
# Solid check
|
|
682
|
+
solid = 0
|
|
683
|
+
if val != 0:
|
|
684
|
+
solid = 1
|
|
685
|
+
is_solid[i, j, k] = solid
|
|
686
|
+
|
|
687
|
+
# Target check
|
|
688
|
+
target = 0
|
|
689
|
+
for t in range(n_targets):
|
|
690
|
+
if val == target_values[t]:
|
|
691
|
+
target = 1
|
|
692
|
+
is_target[i, j, k] = target
|
|
693
|
+
|
|
694
|
+
# Opaque: non-zero, non-tree, non-target (for inclusion mode)
|
|
695
|
+
opaque = 0
|
|
696
|
+
if inclusion_mode == 1:
|
|
697
|
+
if val != 0 and tree == 0 and target == 0:
|
|
698
|
+
opaque = 1
|
|
699
|
+
else:
|
|
700
|
+
if val != 0 and tree == 0:
|
|
701
|
+
opaque = 1
|
|
702
|
+
is_opaque[i, j, k] = opaque
|
|
703
|
+
|
|
704
|
+
@ti.kernel
|
|
705
|
+
def _compute_surface_vf_kernel(
|
|
706
|
+
self,
|
|
707
|
+
face_centers: ti.template(),
|
|
708
|
+
face_normals: ti.template(),
|
|
709
|
+
face_vf_values: ti.template(),
|
|
710
|
+
is_tree: ti.template(),
|
|
711
|
+
is_solid: ti.template(),
|
|
712
|
+
is_target: ti.template(),
|
|
713
|
+
is_opaque: ti.template(),
|
|
714
|
+
tree_att: ti.f32,
|
|
715
|
+
att_cutoff: ti.f32,
|
|
716
|
+
inclusion_mode: ti.i32,
|
|
717
|
+
trees_are_targets: ti.i32,
|
|
718
|
+
grid_bounds: ti.types.ndarray(),
|
|
719
|
+
boundary_epsilon: ti.f32
|
|
720
|
+
):
|
|
721
|
+
"""Compute surface view factors using GPU parallel processing."""
|
|
722
|
+
for f in face_vf_values:
|
|
723
|
+
center = face_centers[f]
|
|
724
|
+
normal = face_normals[f]
|
|
725
|
+
|
|
726
|
+
# Check if face is on domain boundary (vertical and on edge)
|
|
727
|
+
is_vertical = ti.abs(normal[2]) < 0.01
|
|
728
|
+
on_x_min = ti.abs(center[0] - grid_bounds[0, 0]) < boundary_epsilon
|
|
729
|
+
on_y_min = ti.abs(center[1] - grid_bounds[0, 1]) < boundary_epsilon
|
|
730
|
+
on_x_max = ti.abs(center[0] - grid_bounds[1, 0]) < boundary_epsilon
|
|
731
|
+
on_y_max = ti.abs(center[1] - grid_bounds[1, 1]) < boundary_epsilon
|
|
732
|
+
|
|
733
|
+
is_boundary = is_vertical and (on_x_min or on_y_min or on_x_max or on_y_max)
|
|
734
|
+
|
|
735
|
+
if is_boundary:
|
|
736
|
+
face_vf_values[f] = ti.cast(float('nan'), ti.f32)
|
|
737
|
+
else:
|
|
738
|
+
# Build local coordinate system for face
|
|
739
|
+
u, v, n = self._build_face_basis(normal)
|
|
740
|
+
|
|
741
|
+
# Origin: face center offset by normal (in voxel coordinates)
|
|
742
|
+
meshsize = self.dx
|
|
743
|
+
ox = center[0] / meshsize + n[0] * 0.51
|
|
744
|
+
oy = center[1] / meshsize + n[1] * 0.51
|
|
745
|
+
oz = center[2] / meshsize + n[2] * 0.51
|
|
746
|
+
|
|
747
|
+
vis_sum = 0.0
|
|
748
|
+
valid_count = 0
|
|
749
|
+
|
|
750
|
+
# Trace rays in hemisphere
|
|
751
|
+
for r in range(self._n_hemisphere_dirs):
|
|
752
|
+
local_dir = self._hemisphere_dirs[r]
|
|
753
|
+
|
|
754
|
+
# Transform to world direction
|
|
755
|
+
dx = u[0]*local_dir[0] + v[0]*local_dir[1] + n[0]*local_dir[2]
|
|
756
|
+
dy = u[1]*local_dir[0] + v[1]*local_dir[1] + n[1]*local_dir[2]
|
|
757
|
+
dz = u[2]*local_dir[0] + v[2]*local_dir[1] + n[2]*local_dir[2]
|
|
758
|
+
|
|
759
|
+
world_dir = ti.Vector([dx, dy, dz])
|
|
760
|
+
|
|
761
|
+
# Only trace rays going outward from surface
|
|
762
|
+
dot = world_dir.dot(n)
|
|
763
|
+
if dot > 0.0:
|
|
764
|
+
contrib = self._trace_surface_ray(
|
|
765
|
+
ox, oy, oz, world_dir,
|
|
766
|
+
is_tree, is_solid, is_target, is_opaque,
|
|
767
|
+
tree_att, att_cutoff, inclusion_mode, trees_are_targets
|
|
768
|
+
)
|
|
769
|
+
vis_sum += contrib
|
|
770
|
+
valid_count += 1
|
|
771
|
+
|
|
772
|
+
if valid_count > 0:
|
|
773
|
+
face_vf_values[f] = vis_sum / valid_count
|
|
774
|
+
else:
|
|
775
|
+
face_vf_values[f] = 0.0
|
|
776
|
+
|
|
777
|
+
@ti.func
|
|
778
|
+
def _build_face_basis(self, normal: ti.template()) -> ti.template():
|
|
779
|
+
"""Build orthonormal basis (u, v, n) for a surface normal."""
|
|
780
|
+
nrm = normal.norm()
|
|
781
|
+
n = normal
|
|
782
|
+
u = ti.Vector([1.0, 0.0, 0.0])
|
|
783
|
+
v = ti.Vector([0.0, 1.0, 0.0])
|
|
784
|
+
|
|
785
|
+
if nrm > 1e-12:
|
|
786
|
+
n = normal / nrm
|
|
787
|
+
|
|
788
|
+
# Choose helper vector
|
|
789
|
+
helper = ti.Vector([0.0, 0.0, 1.0])
|
|
790
|
+
if ti.abs(n[2]) >= 0.999:
|
|
791
|
+
helper = ti.Vector([1.0, 0.0, 0.0])
|
|
792
|
+
|
|
793
|
+
# u = helper x n
|
|
794
|
+
u = helper.cross(n)
|
|
795
|
+
ul = u.norm()
|
|
796
|
+
if ul > 1e-12:
|
|
797
|
+
u = u / ul
|
|
798
|
+
else:
|
|
799
|
+
u = ti.Vector([1.0, 0.0, 0.0])
|
|
800
|
+
|
|
801
|
+
# v = n x u
|
|
802
|
+
v = n.cross(u)
|
|
803
|
+
|
|
804
|
+
return u, v, n
|
|
805
|
+
|
|
806
|
+
@ti.func
|
|
807
|
+
def _trace_surface_ray(
|
|
808
|
+
self,
|
|
809
|
+
ox: ti.f32,
|
|
810
|
+
oy: ti.f32,
|
|
811
|
+
oz: ti.f32,
|
|
812
|
+
ray_dir: ti.template(),
|
|
813
|
+
is_tree: ti.template(),
|
|
814
|
+
is_solid: ti.template(),
|
|
815
|
+
is_target: ti.template(),
|
|
816
|
+
is_opaque: ti.template(),
|
|
817
|
+
tree_att: ti.f32,
|
|
818
|
+
att_cutoff: ti.f32,
|
|
819
|
+
inclusion_mode: ti.i32,
|
|
820
|
+
trees_are_targets: ti.i32
|
|
821
|
+
) -> ti.f32:
|
|
822
|
+
"""Trace ray from surface for view factor calculation."""
|
|
823
|
+
T = 1.0
|
|
824
|
+
result = 0.0
|
|
825
|
+
|
|
826
|
+
x = ox
|
|
827
|
+
y = oy
|
|
828
|
+
z = oz
|
|
829
|
+
|
|
830
|
+
i = ti.cast(ti.floor(ox), ti.i32)
|
|
831
|
+
j = ti.cast(ti.floor(oy), ti.i32)
|
|
832
|
+
k = ti.cast(ti.floor(oz), ti.i32)
|
|
833
|
+
|
|
834
|
+
step_x = 1 if ray_dir[0] >= 0 else -1
|
|
835
|
+
step_y = 1 if ray_dir[1] >= 0 else -1
|
|
836
|
+
step_z = 1 if ray_dir[2] >= 0 else -1
|
|
837
|
+
|
|
838
|
+
BIG = 1e30
|
|
839
|
+
t_max_x, t_max_y, t_max_z = BIG, BIG, BIG
|
|
840
|
+
t_delta_x, t_delta_y, t_delta_z = BIG, BIG, BIG
|
|
841
|
+
|
|
842
|
+
if ti.abs(ray_dir[0]) > 1e-10:
|
|
843
|
+
t_max_x = ((i + (1 if step_x > 0 else 0)) - x) / ray_dir[0]
|
|
844
|
+
t_delta_x = ti.abs(1.0 / ray_dir[0])
|
|
845
|
+
if ti.abs(ray_dir[1]) > 1e-10:
|
|
846
|
+
t_max_y = ((j + (1 if step_y > 0 else 0)) - y) / ray_dir[1]
|
|
847
|
+
t_delta_y = ti.abs(1.0 / ray_dir[1])
|
|
848
|
+
if ti.abs(ray_dir[2]) > 1e-10:
|
|
849
|
+
t_max_z = ((k + (1 if step_z > 0 else 0)) - z) / ray_dir[2]
|
|
850
|
+
t_delta_z = ti.abs(1.0 / ray_dir[2])
|
|
851
|
+
|
|
852
|
+
max_steps = self.nx + self.ny + self.nz
|
|
853
|
+
done = 0
|
|
854
|
+
|
|
855
|
+
for _ in range(max_steps):
|
|
856
|
+
if done == 0:
|
|
857
|
+
# Bounds check - ray escaped
|
|
858
|
+
if i < 0 or i >= self.nx or j < 0 or j >= self.ny or k < 0 or k >= self.nz:
|
|
859
|
+
if inclusion_mode == 1:
|
|
860
|
+
result = 0.0
|
|
861
|
+
else:
|
|
862
|
+
result = T
|
|
863
|
+
done = 1
|
|
864
|
+
else:
|
|
865
|
+
# Check opaque (blocker)
|
|
866
|
+
if is_opaque[i, j, k] == 1:
|
|
867
|
+
result = 0.0
|
|
868
|
+
done = 1
|
|
869
|
+
elif is_tree[i, j, k] == 1:
|
|
870
|
+
# Tree attenuation
|
|
871
|
+
T *= tree_att
|
|
872
|
+
if T < att_cutoff:
|
|
873
|
+
result = 0.0
|
|
874
|
+
done = 1
|
|
875
|
+
elif trees_are_targets == 1:
|
|
876
|
+
# Trees count as partial visibility
|
|
877
|
+
result = 1.0 - T
|
|
878
|
+
done = 1
|
|
879
|
+
elif inclusion_mode == 1 and is_target[i, j, k] == 1:
|
|
880
|
+
# Hit target in inclusion mode
|
|
881
|
+
result = 1.0
|
|
882
|
+
done = 1
|
|
883
|
+
|
|
884
|
+
if done == 0:
|
|
885
|
+
# Step to next voxel
|
|
886
|
+
if t_max_x < t_max_y:
|
|
887
|
+
if t_max_x < t_max_z:
|
|
888
|
+
t_max_x += t_delta_x
|
|
889
|
+
i += step_x
|
|
890
|
+
else:
|
|
891
|
+
t_max_z += t_delta_z
|
|
892
|
+
k += step_z
|
|
893
|
+
else:
|
|
894
|
+
if t_max_y < t_max_z:
|
|
895
|
+
t_max_y += t_delta_y
|
|
896
|
+
j += step_y
|
|
897
|
+
else:
|
|
898
|
+
t_max_z += t_delta_z
|
|
899
|
+
k += step_z
|
|
900
|
+
|
|
901
|
+
return result
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
# Convenience functions
|
|
905
|
+
def compute_view_index_map(
|
|
906
|
+
domain,
|
|
907
|
+
voxel_data: np.ndarray = None,
|
|
908
|
+
mode: str = 'green',
|
|
909
|
+
**kwargs
|
|
910
|
+
) -> np.ndarray:
|
|
911
|
+
"""
|
|
912
|
+
Compute View Index map.
|
|
913
|
+
|
|
914
|
+
Args:
|
|
915
|
+
domain: Domain object
|
|
916
|
+
voxel_data: 3D voxel class array
|
|
917
|
+
mode: 'green', 'sky', or custom
|
|
918
|
+
**kwargs: Additional parameters for ViewCalculator
|
|
919
|
+
|
|
920
|
+
Returns:
|
|
921
|
+
2D view index map
|
|
922
|
+
"""
|
|
923
|
+
calc = ViewCalculator(domain)
|
|
924
|
+
return calc.compute_view_index(voxel_data=voxel_data, mode=mode, **kwargs)
|
|
925
|
+
|
|
926
|
+
|
|
927
|
+
def compute_sky_view_factor_map(
|
|
928
|
+
domain,
|
|
929
|
+
voxel_data: np.ndarray = None,
|
|
930
|
+
**kwargs
|
|
931
|
+
) -> np.ndarray:
|
|
932
|
+
"""
|
|
933
|
+
Compute Sky View Factor map.
|
|
934
|
+
|
|
935
|
+
Args:
|
|
936
|
+
domain: Domain object
|
|
937
|
+
voxel_data: 3D voxel class array
|
|
938
|
+
**kwargs: Additional parameters
|
|
939
|
+
|
|
940
|
+
Returns:
|
|
941
|
+
2D SVF map
|
|
942
|
+
"""
|
|
943
|
+
calc = ViewCalculator(domain)
|
|
944
|
+
return calc.compute_sky_view_factor(voxel_data=voxel_data, **kwargs)
|