voxcity 1.0.2__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/downloader/ocean.py +559 -0
- voxcity/generator/api.py +6 -0
- voxcity/generator/grids.py +45 -32
- voxcity/generator/pipeline.py +327 -27
- voxcity/geoprocessor/draw.py +14 -8
- voxcity/geoprocessor/raster/__init__.py +2 -0
- voxcity/geoprocessor/raster/core.py +31 -0
- voxcity/geoprocessor/raster/landcover.py +173 -49
- voxcity/geoprocessor/raster/raster.py +1 -1
- voxcity/models.py +2 -0
- 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/visualizer/renderer.py +2 -1
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/METADATA +16 -53
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/RECORD +50 -15
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/WHEEL +0 -0
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/licenses/AUTHORS.rst +0 -0
- {voxcity-1.0.2.dist-info → voxcity-1.0.13.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""VoxCity-style `temporal` module (toplevel) for compatibility."""
|
|
2
|
+
|
|
3
|
+
from .solar.integration import (
|
|
4
|
+
get_solar_positions_astral,
|
|
5
|
+
get_cumulative_global_solar_irradiance,
|
|
6
|
+
get_cumulative_building_solar_irradiance,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"get_solar_positions_astral",
|
|
11
|
+
"get_cumulative_global_solar_irradiance",
|
|
12
|
+
"get_cumulative_building_solar_irradiance",
|
|
13
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Small compatibility module matching `voxcity.simulator.utils`.
|
|
2
|
+
|
|
3
|
+
VoxCity's `voxcity.simulator` flattens `utils` into the toplevel namespace.
|
|
4
|
+
Some user code may rely on these names existing after:
|
|
5
|
+
|
|
6
|
+
import simulator_gpu as simulator
|
|
7
|
+
|
|
8
|
+
This module keeps that behavior without pulling in VoxCity.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import pandas as pd # type: ignore
|
|
17
|
+
except Exception: # pragma: no cover
|
|
18
|
+
pd = None # type: ignore
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def dummy_function(test_string):
|
|
22
|
+
return test_string
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
__all__ = ["np", "pd", "datetime", "dummy_function"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Compatibility wrapper for the legacy VoxCity `view` module.
|
|
2
|
+
|
|
3
|
+
VoxCity exposes view-related functions both under:
|
|
4
|
+
- `voxcity.simulator.visibility.*` (newer)
|
|
5
|
+
- `voxcity.simulator.view.*` (legacy wrapper)
|
|
6
|
+
|
|
7
|
+
This module mirrors that pattern for `simulator_gpu` so code can do:
|
|
8
|
+
import simulator_gpu as simulator
|
|
9
|
+
simulator.view.get_view_index(...)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .visibility import (
|
|
13
|
+
get_view_index,
|
|
14
|
+
get_sky_view_factor_map,
|
|
15
|
+
get_surface_view_factor,
|
|
16
|
+
get_landmark_visibility_map,
|
|
17
|
+
get_surface_landmark_visibility,
|
|
18
|
+
mark_building_by_id,
|
|
19
|
+
compute_landmark_visibility,
|
|
20
|
+
rotate_vector_axis_angle,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"get_view_index",
|
|
25
|
+
"get_sky_view_factor_map",
|
|
26
|
+
"get_surface_view_factor",
|
|
27
|
+
"mark_building_by_id",
|
|
28
|
+
"compute_landmark_visibility",
|
|
29
|
+
"get_landmark_visibility_map",
|
|
30
|
+
"get_surface_landmark_visibility",
|
|
31
|
+
"rotate_vector_axis_angle",
|
|
32
|
+
]
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
simulator_gpu.visibility: GPU-accelerated visibility analysis module.
|
|
3
|
+
|
|
4
|
+
This package emulates voxcity.simulator.visibility using Taichi GPU acceleration.
|
|
5
|
+
|
|
6
|
+
Features:
|
|
7
|
+
- View Index calculation (green view, sky view, custom targets)
|
|
8
|
+
- Sky View Factor calculation
|
|
9
|
+
- Landmark visibility analysis
|
|
10
|
+
- Surface view factor computation
|
|
11
|
+
|
|
12
|
+
API Compatibility:
|
|
13
|
+
This module provides GPU-accelerated versions of the voxcity.simulator.visibility
|
|
14
|
+
API functions. The main functions mirror the original API:
|
|
15
|
+
|
|
16
|
+
- get_view_index() -> GPU version of voxcity.simulator.visibility.get_view_index
|
|
17
|
+
- get_sky_view_factor_map() -> GPU version of voxcity.simulator.visibility.get_sky_view_factor_map
|
|
18
|
+
- get_surface_view_factor() -> GPU version of voxcity.simulator.visibility.get_surface_view_factor
|
|
19
|
+
- get_landmark_visibility_map() -> GPU version of voxcity.simulator.visibility.get_landmark_visibility_map
|
|
20
|
+
- get_surface_landmark_visibility() -> GPU version of voxcity.simulator.visibility.get_surface_landmark_visibility
|
|
21
|
+
- mark_building_by_id() -> Same as voxcity.simulator.visibility.mark_building_by_id
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
from simulator_gpu.visibility import get_view_index, get_sky_view_factor_map
|
|
25
|
+
|
|
26
|
+
vi_map = get_view_index(voxcity, mode='green')
|
|
27
|
+
svf_map = get_sky_view_factor_map(voxcity)
|
|
28
|
+
|
|
29
|
+
# Or use the class-based API:
|
|
30
|
+
from simulator_gpu.visibility import ViewCalculator
|
|
31
|
+
calc = ViewCalculator(domain)
|
|
32
|
+
view_map = calc.compute_view_index(mode='green')
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from .view import (
|
|
36
|
+
ViewCalculator,
|
|
37
|
+
compute_view_index_map,
|
|
38
|
+
compute_sky_view_factor_map,
|
|
39
|
+
SurfaceViewFactorCalculator,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from .landmark import (
|
|
43
|
+
LandmarkVisibilityCalculator,
|
|
44
|
+
compute_landmark_visibility_map,
|
|
45
|
+
mark_building_by_id,
|
|
46
|
+
SurfaceLandmarkVisibilityCalculator,
|
|
47
|
+
compute_landmark_visibility,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from .geometry import (
|
|
51
|
+
generate_ray_directions_grid,
|
|
52
|
+
generate_ray_directions_fibonacci,
|
|
53
|
+
rotate_vector_axis_angle,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
from .integration import (
|
|
57
|
+
# VoxCity API-compatible functions (main interface)
|
|
58
|
+
get_view_index,
|
|
59
|
+
get_sky_view_factor_map,
|
|
60
|
+
get_surface_view_factor,
|
|
61
|
+
get_landmark_visibility_map,
|
|
62
|
+
get_surface_landmark_visibility,
|
|
63
|
+
# Legacy GPU-suffixed functions (backward compatibility)
|
|
64
|
+
get_view_index_gpu,
|
|
65
|
+
get_sky_view_factor_map_gpu,
|
|
66
|
+
get_landmark_visibility_map_gpu,
|
|
67
|
+
# Utility functions
|
|
68
|
+
create_domain_from_voxcity,
|
|
69
|
+
mark_building_by_id,
|
|
70
|
+
# Constants
|
|
71
|
+
VOXCITY_GROUND_CODE,
|
|
72
|
+
VOXCITY_TREE_CODE,
|
|
73
|
+
VOXCITY_BUILDING_CODE,
|
|
74
|
+
GREEN_VIEW_CODES,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
__all__ = [
|
|
78
|
+
# VoxCity API-compatible functions (recommended)
|
|
79
|
+
'get_view_index',
|
|
80
|
+
'get_sky_view_factor_map',
|
|
81
|
+
'get_surface_view_factor',
|
|
82
|
+
'get_landmark_visibility_map',
|
|
83
|
+
'get_surface_landmark_visibility',
|
|
84
|
+
'mark_building_by_id',
|
|
85
|
+
'compute_landmark_visibility',
|
|
86
|
+
# Geometry helpers (matches voxcity.simulator.common.geometry)
|
|
87
|
+
'rotate_vector_axis_angle',
|
|
88
|
+
# Main calculators (class-based API)
|
|
89
|
+
'ViewCalculator',
|
|
90
|
+
'LandmarkVisibilityCalculator',
|
|
91
|
+
'SurfaceViewFactorCalculator',
|
|
92
|
+
'SurfaceLandmarkVisibilityCalculator',
|
|
93
|
+
# Functions
|
|
94
|
+
'compute_view_index_map',
|
|
95
|
+
'compute_sky_view_factor_map',
|
|
96
|
+
'compute_landmark_visibility_map',
|
|
97
|
+
# Geometry helpers
|
|
98
|
+
'generate_ray_directions_grid',
|
|
99
|
+
'generate_ray_directions_fibonacci',
|
|
100
|
+
# VoxCity integration (legacy, backward compatibility)
|
|
101
|
+
'create_domain_from_voxcity',
|
|
102
|
+
'get_view_index_gpu',
|
|
103
|
+
'get_sky_view_factor_map_gpu',
|
|
104
|
+
'get_landmark_visibility_map_gpu',
|
|
105
|
+
'VOXCITY_GROUND_CODE',
|
|
106
|
+
'VOXCITY_TREE_CODE',
|
|
107
|
+
'VOXCITY_BUILDING_CODE',
|
|
108
|
+
'GREEN_VIEW_CODES',
|
|
109
|
+
]
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geometry utilities for view analysis.
|
|
3
|
+
|
|
4
|
+
Provides ray direction generation using various sampling strategies:
|
|
5
|
+
- Grid-based sampling (uniform azimuth x elevation grid)
|
|
6
|
+
- Fibonacci spiral sampling (more uniform hemisphere coverage)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
import taichi as ti
|
|
11
|
+
from typing import Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def generate_ray_directions_grid(
|
|
15
|
+
n_azimuth: int = 120,
|
|
16
|
+
n_elevation: int = 20,
|
|
17
|
+
elevation_min_degrees: float = -30.0,
|
|
18
|
+
elevation_max_degrees: float = 30.0
|
|
19
|
+
) -> np.ndarray:
|
|
20
|
+
"""
|
|
21
|
+
Generate ray directions using a regular grid sampling.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
n_azimuth: Number of azimuthal divisions
|
|
25
|
+
n_elevation: Number of elevation divisions
|
|
26
|
+
elevation_min_degrees: Minimum elevation angle in degrees
|
|
27
|
+
elevation_max_degrees: Maximum elevation angle in degrees
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Array of shape (n_azimuth * n_elevation, 3) with unit direction vectors
|
|
31
|
+
"""
|
|
32
|
+
azimuth_angles = np.linspace(0.0, 2.0 * np.pi, int(n_azimuth), endpoint=False)
|
|
33
|
+
elevation_angles = np.deg2rad(
|
|
34
|
+
np.linspace(float(elevation_min_degrees), float(elevation_max_degrees), int(n_elevation))
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
ray_directions = np.empty((len(azimuth_angles) * len(elevation_angles), 3), dtype=np.float32)
|
|
38
|
+
out_idx = 0
|
|
39
|
+
|
|
40
|
+
for elevation in elevation_angles:
|
|
41
|
+
cos_elev = np.cos(elevation)
|
|
42
|
+
sin_elev = np.sin(elevation)
|
|
43
|
+
for azimuth in azimuth_angles:
|
|
44
|
+
# x = east, y = north, z = up
|
|
45
|
+
dx = cos_elev * np.sin(azimuth)
|
|
46
|
+
dy = cos_elev * np.cos(azimuth)
|
|
47
|
+
dz = sin_elev
|
|
48
|
+
ray_directions[out_idx, 0] = dx
|
|
49
|
+
ray_directions[out_idx, 1] = dy
|
|
50
|
+
ray_directions[out_idx, 2] = dz
|
|
51
|
+
out_idx += 1
|
|
52
|
+
|
|
53
|
+
return ray_directions
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def generate_ray_directions_fibonacci(
|
|
57
|
+
n_rays: int = 2400,
|
|
58
|
+
elevation_min_degrees: float = -30.0,
|
|
59
|
+
elevation_max_degrees: float = 30.0
|
|
60
|
+
) -> np.ndarray:
|
|
61
|
+
"""
|
|
62
|
+
Generate ray directions using Fibonacci spiral sampling.
|
|
63
|
+
|
|
64
|
+
This provides more uniform coverage of the hemisphere compared to grid sampling.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
n_rays: Total number of rays
|
|
68
|
+
elevation_min_degrees: Minimum elevation angle in degrees
|
|
69
|
+
elevation_max_degrees: Maximum elevation angle in degrees
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Array of shape (n_rays, 3) with unit direction vectors
|
|
73
|
+
"""
|
|
74
|
+
N = int(max(1, n_rays))
|
|
75
|
+
emin = np.deg2rad(float(elevation_min_degrees))
|
|
76
|
+
emax = np.deg2rad(float(elevation_max_degrees))
|
|
77
|
+
|
|
78
|
+
z_min = np.sin(min(emin, emax))
|
|
79
|
+
z_max = np.sin(max(emin, emax))
|
|
80
|
+
|
|
81
|
+
golden_angle = np.pi * (3.0 - np.sqrt(5.0))
|
|
82
|
+
|
|
83
|
+
i = np.arange(N, dtype=np.float64)
|
|
84
|
+
z = z_min + (i + 0.5) * (z_max - z_min) / N
|
|
85
|
+
phi = i * golden_angle
|
|
86
|
+
|
|
87
|
+
r = np.sqrt(np.clip(1.0 - z * z, 0.0, 1.0))
|
|
88
|
+
x = r * np.cos(phi)
|
|
89
|
+
y = r * np.sin(phi)
|
|
90
|
+
|
|
91
|
+
return np.stack((x, y, z), axis=1).astype(np.float32)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def generate_hemisphere_directions(
|
|
95
|
+
n_azimuth: int = 80,
|
|
96
|
+
n_elevation: int = 40
|
|
97
|
+
) -> np.ndarray:
|
|
98
|
+
"""
|
|
99
|
+
Generate ray directions covering the upper hemisphere.
|
|
100
|
+
|
|
101
|
+
Elevation goes from 0 (horizon) to 90 (zenith).
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
n_azimuth: Number of azimuthal divisions
|
|
105
|
+
n_elevation: Number of elevation divisions
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Array of shape (n_azimuth * n_elevation, 3) with unit direction vectors
|
|
109
|
+
"""
|
|
110
|
+
return generate_ray_directions_grid(
|
|
111
|
+
n_azimuth=n_azimuth,
|
|
112
|
+
n_elevation=n_elevation,
|
|
113
|
+
elevation_min_degrees=0.0,
|
|
114
|
+
elevation_max_degrees=90.0
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def rotate_vector_axis_angle(vec: np.ndarray, axis: np.ndarray, angle: float) -> np.ndarray:
|
|
119
|
+
"""
|
|
120
|
+
Rotate a vector around an axis by a specified angle using Rodrigues' rotation formula.
|
|
121
|
+
|
|
122
|
+
This is a CPU implementation matching voxcity.simulator.common.geometry.rotate_vector_axis_angle.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
vec: Vector to rotate (3,)
|
|
126
|
+
axis: Rotation axis (3,) - will be normalized
|
|
127
|
+
angle: Rotation angle in radians
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Rotated vector (3,)
|
|
131
|
+
"""
|
|
132
|
+
axis_len = np.sqrt(axis[0]**2 + axis[1]**2 + axis[2]**2)
|
|
133
|
+
if axis_len < 1e-12:
|
|
134
|
+
return vec.copy()
|
|
135
|
+
ux, uy, uz = axis / axis_len
|
|
136
|
+
c = np.cos(angle)
|
|
137
|
+
s = np.sin(angle)
|
|
138
|
+
dot = vec[0]*ux + vec[1]*uy + vec[2]*uz
|
|
139
|
+
cross_x = uy*vec[2] - uz*vec[1]
|
|
140
|
+
cross_y = uz*vec[0] - ux*vec[2]
|
|
141
|
+
cross_z = ux*vec[1] - uy*vec[0]
|
|
142
|
+
v_rot = np.zeros(3, dtype=np.float64)
|
|
143
|
+
v_rot[0] = vec[0] * c + cross_x * s + ux * dot * (1.0 - c)
|
|
144
|
+
v_rot[1] = vec[1] * c + cross_y * s + uy * dot * (1.0 - c)
|
|
145
|
+
v_rot[2] = vec[2] * c + cross_z * s + uz * dot * (1.0 - c)
|
|
146
|
+
return v_rot
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@ti.func
|
|
150
|
+
def rotate_vector_axis_angle_ti(vec: ti.template(), axis: ti.template(), angle: ti.f32) -> ti.template():
|
|
151
|
+
"""
|
|
152
|
+
Taichi GPU kernel function for rotating a vector around an axis.
|
|
153
|
+
|
|
154
|
+
Uses Rodrigues' rotation formula.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
vec: Vector to rotate (Vector3)
|
|
158
|
+
axis: Rotation axis (Vector3) - will be normalized
|
|
159
|
+
angle: Rotation angle in radians
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
Rotated Vector3
|
|
163
|
+
"""
|
|
164
|
+
result = vec
|
|
165
|
+
axis_len = axis.norm()
|
|
166
|
+
if axis_len > 1e-12:
|
|
167
|
+
u = axis / axis_len
|
|
168
|
+
c = ti.cos(angle)
|
|
169
|
+
s = ti.sin(angle)
|
|
170
|
+
dot = vec.dot(u)
|
|
171
|
+
cross = u.cross(vec)
|
|
172
|
+
result = vec * c + cross * s + u * dot * (1.0 - c)
|
|
173
|
+
return result
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def build_face_basis(normal: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
177
|
+
"""
|
|
178
|
+
Build orthonormal basis (u, v, n) for a surface normal.
|
|
179
|
+
|
|
180
|
+
Matches voxcity.simulator.common.geometry._build_face_basis.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
normal: Surface normal vector (3,)
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Tuple of (u, v, n) orthonormal vectors
|
|
187
|
+
"""
|
|
188
|
+
nx, ny, nz = normal
|
|
189
|
+
nrm = np.sqrt(nx*nx + ny*ny + nz*nz)
|
|
190
|
+
if nrm < 1e-12:
|
|
191
|
+
return (np.array([1.0, 0.0, 0.0]),
|
|
192
|
+
np.array([0.0, 1.0, 0.0]),
|
|
193
|
+
np.array([0.0, 0.0, 1.0]))
|
|
194
|
+
invn = 1.0 / nrm
|
|
195
|
+
nx *= invn
|
|
196
|
+
ny *= invn
|
|
197
|
+
nz *= invn
|
|
198
|
+
n = np.array([nx, ny, nz])
|
|
199
|
+
|
|
200
|
+
# Choose helper vector to cross with normal
|
|
201
|
+
if abs(nz) < 0.999:
|
|
202
|
+
helper = np.array([0.0, 0.0, 1.0])
|
|
203
|
+
else:
|
|
204
|
+
helper = np.array([1.0, 0.0, 0.0])
|
|
205
|
+
|
|
206
|
+
# u = helper x n (normalized)
|
|
207
|
+
u = np.cross(helper, n)
|
|
208
|
+
ul = np.linalg.norm(u)
|
|
209
|
+
if ul < 1e-12:
|
|
210
|
+
u = np.array([1.0, 0.0, 0.0])
|
|
211
|
+
else:
|
|
212
|
+
u = u / ul
|
|
213
|
+
|
|
214
|
+
# v = n x u
|
|
215
|
+
v = np.cross(n, u)
|
|
216
|
+
|
|
217
|
+
return u, v, n
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@ti.func
|
|
221
|
+
def build_face_basis_ti(normal: ti.template()) -> ti.template():
|
|
222
|
+
"""
|
|
223
|
+
Taichi GPU kernel function to build orthonormal basis for a surface normal.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
normal: Surface normal Vector3
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Tuple of (u, v, n) orthonormal Vector3s
|
|
230
|
+
"""
|
|
231
|
+
nrm = normal.norm()
|
|
232
|
+
n = normal
|
|
233
|
+
u = ti.Vector([1.0, 0.0, 0.0])
|
|
234
|
+
v = ti.Vector([0.0, 1.0, 0.0])
|
|
235
|
+
|
|
236
|
+
if nrm > 1e-12:
|
|
237
|
+
n = normal / nrm
|
|
238
|
+
|
|
239
|
+
# Choose helper vector
|
|
240
|
+
helper = ti.Vector([0.0, 0.0, 1.0])
|
|
241
|
+
if ti.abs(n[2]) >= 0.999:
|
|
242
|
+
helper = ti.Vector([1.0, 0.0, 0.0])
|
|
243
|
+
|
|
244
|
+
# u = helper x n
|
|
245
|
+
u = helper.cross(n)
|
|
246
|
+
ul = u.norm()
|
|
247
|
+
if ul > 1e-12:
|
|
248
|
+
u = u / ul
|
|
249
|
+
else:
|
|
250
|
+
u = ti.Vector([1.0, 0.0, 0.0])
|
|
251
|
+
|
|
252
|
+
# v = n x u
|
|
253
|
+
v = n.cross(u)
|
|
254
|
+
|
|
255
|
+
return u, v, n
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@ti.data_oriented
|
|
259
|
+
class RayDirectionField:
|
|
260
|
+
"""
|
|
261
|
+
GPU-accessible ray direction field for batch processing.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
def __init__(self, directions: np.ndarray):
|
|
265
|
+
"""
|
|
266
|
+
Initialize with numpy array of directions.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
directions: Array of shape (n_rays, 3)
|
|
270
|
+
"""
|
|
271
|
+
self.n_rays = directions.shape[0]
|
|
272
|
+
self.directions = ti.Vector.field(3, dtype=ti.f32, shape=(self.n_rays,))
|
|
273
|
+
self._init_from_numpy(directions)
|
|
274
|
+
|
|
275
|
+
@ti.kernel
|
|
276
|
+
def _init_from_numpy(self, dirs: ti.types.ndarray()):
|
|
277
|
+
for i in range(self.n_rays):
|
|
278
|
+
self.directions[i] = ti.Vector([dirs[i, 0], dirs[i, 1], dirs[i, 2]])
|