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,115 @@
|
|
|
1
|
+
"""simulator_gpu: GPU-accelerated simulation modules using Taichi.
|
|
2
|
+
|
|
3
|
+
Compatibility goal:
|
|
4
|
+
Allow the common VoxCity pattern to work without code changes beyond the
|
|
5
|
+
import alias:
|
|
6
|
+
|
|
7
|
+
import simulator_gpu as simulator
|
|
8
|
+
|
|
9
|
+
by flattening a VoxCity-like public namespace (view/visibility/solar/utils).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
# Disable Numba caching to prevent stale cache issues when module paths change.
|
|
15
|
+
# This avoids "ModuleNotFoundError: No module named 'simulator_gpu'" errors
|
|
16
|
+
# that can occur when Numba tries to load cached functions with old module paths.
|
|
17
|
+
os.environ.setdefault("NUMBA_CACHE_DIR", "") # Disable disk caching
|
|
18
|
+
os.environ.setdefault("NUMBA_DISABLE_JIT", "0") # Keep JIT enabled for performance
|
|
19
|
+
|
|
20
|
+
# Import Taichi initialization utilities first
|
|
21
|
+
from .init_taichi import ( # noqa: F401
|
|
22
|
+
init_taichi,
|
|
23
|
+
ensure_initialized,
|
|
24
|
+
is_initialized,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Check if Taichi is available
|
|
28
|
+
try:
|
|
29
|
+
import taichi as ti
|
|
30
|
+
_TAICHI_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
_TAICHI_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
# VoxCity-style flattening
|
|
35
|
+
from .view import * # noqa: F401,F403
|
|
36
|
+
from .solar import * # noqa: F401,F403
|
|
37
|
+
from .utils import * # noqa: F401,F403
|
|
38
|
+
|
|
39
|
+
# Export submodules for explicit access
|
|
40
|
+
from . import solar # noqa: F401
|
|
41
|
+
from . import visibility # noqa: F401
|
|
42
|
+
from . import view # noqa: F401
|
|
43
|
+
from . import utils # noqa: F401
|
|
44
|
+
from . import common # noqa: F401
|
|
45
|
+
|
|
46
|
+
# VoxCity-flattened module names that some code expects to exist on the toplevel
|
|
47
|
+
from . import sky # noqa: F401
|
|
48
|
+
from . import kernels # noqa: F401
|
|
49
|
+
from . import radiation # noqa: F401
|
|
50
|
+
from . import temporal # noqa: F401
|
|
51
|
+
from . import integration # noqa: F401
|
|
52
|
+
|
|
53
|
+
# Commonly re-exported VoxCity solar helpers
|
|
54
|
+
from .kernels import compute_direct_solar_irradiance_map_binary # noqa: F401
|
|
55
|
+
from .radiation import compute_solar_irradiance_for_all_faces # noqa: F401
|
|
56
|
+
|
|
57
|
+
# Backward compatibility: some code treats `simulator.view` as `simulator.visibility`
|
|
58
|
+
# (VoxCity provides `view.py` wrapper; we also provide that module).
|
|
59
|
+
|
|
60
|
+
# Export shared modules (kept; extra symbols are fine)
|
|
61
|
+
from .core import ( # noqa: F401
|
|
62
|
+
Vector3, Point3,
|
|
63
|
+
PI, TWO_PI, DEG_TO_RAD, RAD_TO_DEG,
|
|
64
|
+
SOLAR_CONSTANT, EXT_COEF,
|
|
65
|
+
)
|
|
66
|
+
from .domain import Domain, IUP, IDOWN, INORTH, ISOUTH, IEAST, IWEST # noqa: F401
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def clear_numba_cache():
|
|
70
|
+
"""Clear Numba's compiled function cache to resolve stale cache issues.
|
|
71
|
+
|
|
72
|
+
Call this function if you encounter errors like:
|
|
73
|
+
ModuleNotFoundError: No module named 'simulator_gpu'
|
|
74
|
+
|
|
75
|
+
After calling this function, restart your Python kernel/interpreter.
|
|
76
|
+
"""
|
|
77
|
+
import shutil
|
|
78
|
+
import glob
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
|
|
81
|
+
cleared = []
|
|
82
|
+
|
|
83
|
+
# Clear .nbc and .nbi files in the package directory
|
|
84
|
+
package_dir = Path(__file__).parent
|
|
85
|
+
for pattern in ["**/*.nbc", "**/*.nbi"]:
|
|
86
|
+
for cache_file in package_dir.glob(pattern):
|
|
87
|
+
try:
|
|
88
|
+
cache_file.unlink()
|
|
89
|
+
cleared.append(str(cache_file))
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
# Clear __pycache__ directories
|
|
94
|
+
for pycache in package_dir.glob("**/__pycache__"):
|
|
95
|
+
try:
|
|
96
|
+
shutil.rmtree(pycache)
|
|
97
|
+
cleared.append(str(pycache))
|
|
98
|
+
except Exception:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
# Try to clear user's .numba_cache if it exists
|
|
102
|
+
home = Path.home()
|
|
103
|
+
numba_cache = home / ".numba_cache"
|
|
104
|
+
if numba_cache.exists():
|
|
105
|
+
try:
|
|
106
|
+
shutil.rmtree(numba_cache)
|
|
107
|
+
cleared.append(str(numba_cache))
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
print(f"Cleared {len(cleared)} cache items. Please restart your Python kernel.")
|
|
112
|
+
return cleared
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""VoxCity-style `common` namespace.
|
|
2
|
+
|
|
3
|
+
VoxCity exposes helpers under `voxcity.simulator.common.*`.
|
|
4
|
+
`simulator_gpu` implements only the subset needed for drop-in compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .geometry import rotate_vector_axis_angle
|
|
8
|
+
|
|
9
|
+
__all__ = ["rotate_vector_axis_angle"]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Geometry helpers for VoxCity compatibility.
|
|
2
|
+
|
|
3
|
+
This module is intended to satisfy imports like:
|
|
4
|
+
from simulator.common.geometry import rotate_vector_axis_angle
|
|
5
|
+
|
|
6
|
+
It forwards to the implementation used by `simulator_gpu.visibility`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ..visibility.geometry import rotate_vector_axis_angle
|
|
10
|
+
|
|
11
|
+
__all__ = ["rotate_vector_axis_angle"]
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Shared core utilities for simulator_gpu.
|
|
3
|
+
|
|
4
|
+
Vector and ray utilities using Taichi for GPU acceleration.
|
|
5
|
+
Based on ray-tracing-one-weekend-taichi patterns.
|
|
6
|
+
|
|
7
|
+
GPU Optimization Notes:
|
|
8
|
+
- All functions use @ti.func for GPU inlining
|
|
9
|
+
- Branchless operations preferred where possible
|
|
10
|
+
- Memory coalescing friendly access patterns
|
|
11
|
+
- done-flag pattern for early termination (reduces warp divergence)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import taichi as ti
|
|
15
|
+
import math
|
|
16
|
+
|
|
17
|
+
# Type aliases for clarity
|
|
18
|
+
Vector3 = ti.math.vec3
|
|
19
|
+
Point3 = ti.math.vec3
|
|
20
|
+
Color3 = ti.math.vec3
|
|
21
|
+
|
|
22
|
+
# Constants - using Taichi static for compile-time optimization
|
|
23
|
+
PI = math.pi
|
|
24
|
+
TWO_PI = 2.0 * math.pi
|
|
25
|
+
HALF_PI = math.pi / 2.0
|
|
26
|
+
DEG_TO_RAD = math.pi / 180.0
|
|
27
|
+
RAD_TO_DEG = 180.0 / math.pi
|
|
28
|
+
|
|
29
|
+
# Solar constant (W/m^2) - matching PALM's solar_constant
|
|
30
|
+
SOLAR_CONSTANT = 1361.0
|
|
31
|
+
|
|
32
|
+
# Default extinction coefficient for vegetation
|
|
33
|
+
# PALM: ext_coef = 0.6_wp (radiation_model_mod.f90 line ~890)
|
|
34
|
+
EXT_COEF = 0.6
|
|
35
|
+
|
|
36
|
+
# Minimum stable cosine of zenith angle
|
|
37
|
+
MIN_STABLE_COSZEN = 0.0262
|
|
38
|
+
|
|
39
|
+
# GPU block size hint for optimal thread occupancy
|
|
40
|
+
GPU_BLOCK_SIZE = 256
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@ti.func
|
|
44
|
+
def normalize(v: Vector3) -> Vector3:
|
|
45
|
+
"""Normalize a vector."""
|
|
46
|
+
return v / v.norm()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@ti.func
|
|
50
|
+
def normalize_safe(v: Vector3) -> Vector3:
|
|
51
|
+
"""Normalize a vector with safety check for zero-length."""
|
|
52
|
+
len_sq = v.dot(v)
|
|
53
|
+
if len_sq > 1e-10:
|
|
54
|
+
return v / ti.sqrt(len_sq)
|
|
55
|
+
return Vector3(0.0, 0.0, 1.0)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@ti.func
|
|
59
|
+
def dot(v1: Vector3, v2: Vector3) -> ti.f32:
|
|
60
|
+
"""Dot product of two vectors."""
|
|
61
|
+
return v1.dot(v2)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@ti.func
|
|
65
|
+
def cross(v1: Vector3, v2: Vector3) -> Vector3:
|
|
66
|
+
"""Cross product of two vectors."""
|
|
67
|
+
return v1.cross(v2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@ti.func
|
|
71
|
+
def reflect(v: Vector3, n: Vector3) -> Vector3:
|
|
72
|
+
"""Reflect vector v around normal n."""
|
|
73
|
+
return v - 2.0 * v.dot(n) * n
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@ti.func
|
|
77
|
+
def ray_at(origin: Point3, direction: Vector3, t: ti.f32) -> Point3:
|
|
78
|
+
"""Get point along ray at parameter t."""
|
|
79
|
+
return origin + t * direction
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@ti.func
|
|
83
|
+
def length_squared(v: Vector3) -> ti.f32:
|
|
84
|
+
"""Compute squared length of vector (avoids sqrt)."""
|
|
85
|
+
return v.dot(v)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@ti.func
|
|
89
|
+
def distance_squared(p1: Point3, p2: Point3) -> ti.f32:
|
|
90
|
+
"""Compute squared distance between two points (avoids sqrt)."""
|
|
91
|
+
diff = p2 - p1
|
|
92
|
+
return diff.dot(diff)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@ti.func
|
|
96
|
+
def min3(a: ti.f32, b: ti.f32, c: ti.f32) -> ti.f32:
|
|
97
|
+
"""Branchless minimum of three values."""
|
|
98
|
+
return ti.min(a, ti.min(b, c))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@ti.func
|
|
102
|
+
def max3(a: ti.f32, b: ti.f32, c: ti.f32) -> ti.f32:
|
|
103
|
+
"""Branchless maximum of three values."""
|
|
104
|
+
return ti.max(a, ti.max(b, c))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@ti.func
|
|
108
|
+
def clamp(x: ti.f32, lo: ti.f32, hi: ti.f32) -> ti.f32:
|
|
109
|
+
"""Clamp value to range [lo, hi]."""
|
|
110
|
+
return ti.max(lo, ti.min(hi, x))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@ti.func
|
|
114
|
+
def random_in_unit_sphere() -> Vector3:
|
|
115
|
+
"""Generate random point in unit sphere."""
|
|
116
|
+
theta = ti.random() * TWO_PI
|
|
117
|
+
v = ti.random()
|
|
118
|
+
phi = ti.acos(2.0 * v - 1.0)
|
|
119
|
+
r = ti.random() ** (1.0 / 3.0)
|
|
120
|
+
return Vector3(
|
|
121
|
+
r * ti.sin(phi) * ti.cos(theta),
|
|
122
|
+
r * ti.sin(phi) * ti.sin(theta),
|
|
123
|
+
r * ti.cos(phi)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@ti.func
|
|
128
|
+
def random_in_hemisphere(normal: Vector3) -> Vector3:
|
|
129
|
+
"""Generate random vector in hemisphere around normal."""
|
|
130
|
+
vec = random_in_unit_sphere()
|
|
131
|
+
if vec.dot(normal) < 0.0:
|
|
132
|
+
vec = -vec
|
|
133
|
+
return vec
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@ti.func
|
|
137
|
+
def random_cosine_hemisphere(normal: Vector3) -> Vector3:
|
|
138
|
+
"""
|
|
139
|
+
Generate random vector with cosine-weighted distribution in hemisphere.
|
|
140
|
+
Used for diffuse radiation sampling.
|
|
141
|
+
"""
|
|
142
|
+
u1 = ti.random()
|
|
143
|
+
u2 = ti.random()
|
|
144
|
+
r = ti.sqrt(u1)
|
|
145
|
+
theta = TWO_PI * u2
|
|
146
|
+
|
|
147
|
+
x = r * ti.cos(theta)
|
|
148
|
+
y = r * ti.sin(theta)
|
|
149
|
+
z = ti.sqrt(1.0 - u1)
|
|
150
|
+
|
|
151
|
+
# Create orthonormal basis around normal
|
|
152
|
+
up = Vector3(0.0, 1.0, 0.0)
|
|
153
|
+
if ti.abs(normal.y) > 0.999:
|
|
154
|
+
up = Vector3(1.0, 0.0, 0.0)
|
|
155
|
+
|
|
156
|
+
tangent = normalize(cross(up, normal))
|
|
157
|
+
bitangent = cross(normal, tangent)
|
|
158
|
+
|
|
159
|
+
return normalize(x * tangent + y * bitangent + z * normal)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@ti.func
|
|
163
|
+
def spherical_to_cartesian(azimuth: ti.f32, elevation: ti.f32) -> Vector3:
|
|
164
|
+
"""
|
|
165
|
+
Convert spherical coordinates to Cartesian unit vector.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
azimuth: Angle from north (y-axis), clockwise, in radians
|
|
169
|
+
elevation: Angle from horizontal, in radians (0 = horizontal, pi/2 = zenith)
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Unit vector (x, y, z) where z is vertical (up)
|
|
173
|
+
"""
|
|
174
|
+
cos_elev = ti.cos(elevation)
|
|
175
|
+
sin_elev = ti.sin(elevation)
|
|
176
|
+
cos_azim = ti.cos(azimuth)
|
|
177
|
+
sin_azim = ti.sin(azimuth)
|
|
178
|
+
|
|
179
|
+
# x = east, y = north, z = up
|
|
180
|
+
x = cos_elev * sin_azim
|
|
181
|
+
y = cos_elev * cos_azim
|
|
182
|
+
z = sin_elev
|
|
183
|
+
|
|
184
|
+
return Vector3(x, y, z)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@ti.func
|
|
188
|
+
def cartesian_to_spherical(v: Vector3) -> ti.math.vec2:
|
|
189
|
+
"""
|
|
190
|
+
Convert Cartesian unit vector to spherical coordinates.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
vec2(azimuth, elevation) in radians
|
|
194
|
+
"""
|
|
195
|
+
elevation = ti.asin(ti.math.clamp(v.z, -1.0, 1.0))
|
|
196
|
+
azimuth = ti.atan2(v.x, v.y)
|
|
197
|
+
if azimuth < 0.0:
|
|
198
|
+
azimuth += TWO_PI
|
|
199
|
+
return ti.math.vec2(azimuth, elevation)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@ti.func
|
|
203
|
+
def rotate_vector_axis_angle(vec: Vector3, axis: Vector3, angle: ti.f32) -> Vector3:
|
|
204
|
+
"""
|
|
205
|
+
Rotate vector around an axis by a given angle using Rodrigues' rotation formula.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
vec: Vector to rotate
|
|
209
|
+
axis: Rotation axis (will be normalized)
|
|
210
|
+
angle: Rotation angle in radians
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
Rotated vector
|
|
214
|
+
"""
|
|
215
|
+
axis_len = axis.norm()
|
|
216
|
+
if axis_len < 1e-12:
|
|
217
|
+
return vec
|
|
218
|
+
|
|
219
|
+
k = axis / axis_len
|
|
220
|
+
c = ti.cos(angle)
|
|
221
|
+
s = ti.sin(angle)
|
|
222
|
+
|
|
223
|
+
# Rodrigues' rotation formula: v_rot = v*cos(θ) + (k×v)*sin(θ) + k*(k·v)*(1-cos(θ))
|
|
224
|
+
v_rot = vec * c + cross(k, vec) * s + k * dot(k, vec) * (1.0 - c)
|
|
225
|
+
|
|
226
|
+
return v_rot
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
@ti.func
|
|
230
|
+
def build_face_basis(normal: Vector3):
|
|
231
|
+
"""
|
|
232
|
+
Build orthonormal basis for a face with given normal.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Tuple of (tangent, bitangent, normal) vectors
|
|
236
|
+
"""
|
|
237
|
+
n_len = normal.norm()
|
|
238
|
+
if n_len < 1e-12:
|
|
239
|
+
return Vector3(1.0, 0.0, 0.0), Vector3(0.0, 1.0, 0.0), Vector3(0.0, 0.0, 1.0)
|
|
240
|
+
|
|
241
|
+
n = normal / n_len
|
|
242
|
+
|
|
243
|
+
# Choose helper vector not parallel to normal
|
|
244
|
+
helper = Vector3(0.0, 0.0, 1.0)
|
|
245
|
+
if ti.abs(n.z) > 0.999:
|
|
246
|
+
helper = Vector3(1.0, 0.0, 0.0)
|
|
247
|
+
|
|
248
|
+
# Compute tangent via cross product
|
|
249
|
+
u = cross(helper, n)
|
|
250
|
+
u_len = u.norm()
|
|
251
|
+
if u_len < 1e-12:
|
|
252
|
+
u = Vector3(1.0, 0.0, 0.0)
|
|
253
|
+
else:
|
|
254
|
+
u = u / u_len
|
|
255
|
+
|
|
256
|
+
# Bitangent
|
|
257
|
+
v = cross(n, u)
|
|
258
|
+
|
|
259
|
+
return u, v, n
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@ti.data_oriented
|
|
263
|
+
class Rays:
|
|
264
|
+
"""
|
|
265
|
+
Array of rays for batch processing.
|
|
266
|
+
Similar to ray-tracing-one-weekend-taichi but adapted for view/solar tracing.
|
|
267
|
+
"""
|
|
268
|
+
|
|
269
|
+
def __init__(self, n_rays: int):
|
|
270
|
+
self.n_rays = n_rays
|
|
271
|
+
self.origin = ti.Vector.field(3, dtype=ti.f32, shape=(n_rays,))
|
|
272
|
+
self.direction = ti.Vector.field(3, dtype=ti.f32, shape=(n_rays,))
|
|
273
|
+
self.transparency = ti.field(dtype=ti.f32, shape=(n_rays,))
|
|
274
|
+
self.active = ti.field(dtype=ti.i32, shape=(n_rays,))
|
|
275
|
+
|
|
276
|
+
@ti.func
|
|
277
|
+
def set(self, idx: ti.i32, origin: Point3, direction: Vector3, transp: ti.f32):
|
|
278
|
+
self.origin[idx] = origin
|
|
279
|
+
self.direction[idx] = direction
|
|
280
|
+
self.transparency[idx] = transp
|
|
281
|
+
self.active[idx] = 1
|
|
282
|
+
|
|
283
|
+
@ti.func
|
|
284
|
+
def get(self, idx: ti.i32):
|
|
285
|
+
return self.origin[idx], self.direction[idx], self.transparency[idx]
|
|
286
|
+
|
|
287
|
+
@ti.func
|
|
288
|
+
def deactivate(self, idx: ti.i32):
|
|
289
|
+
self.active[idx] = 0
|
|
290
|
+
|
|
291
|
+
@ti.func
|
|
292
|
+
def is_active(self, idx: ti.i32) -> ti.i32:
|
|
293
|
+
return self.active[idx]
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@ti.data_oriented
|
|
297
|
+
class HitRecord:
|
|
298
|
+
"""
|
|
299
|
+
Store ray-surface intersection results.
|
|
300
|
+
"""
|
|
301
|
+
|
|
302
|
+
def __init__(self, n_rays: int):
|
|
303
|
+
self.n_rays = n_rays
|
|
304
|
+
self.hit = ti.field(dtype=ti.i32, shape=(n_rays,))
|
|
305
|
+
self.t = ti.field(dtype=ti.f32, shape=(n_rays,))
|
|
306
|
+
self.point = ti.Vector.field(3, dtype=ti.f32, shape=(n_rays,))
|
|
307
|
+
self.normal = ti.Vector.field(3, dtype=ti.f32, shape=(n_rays,))
|
|
308
|
+
self.surface_id = ti.field(dtype=ti.i32, shape=(n_rays,))
|
|
309
|
+
|
|
310
|
+
@ti.func
|
|
311
|
+
def set(self, idx: ti.i32, hit: ti.i32, t: ti.f32, point: Point3,
|
|
312
|
+
normal: Vector3, surface_id: ti.i32):
|
|
313
|
+
self.hit[idx] = hit
|
|
314
|
+
self.t[idx] = t
|
|
315
|
+
self.point[idx] = point
|
|
316
|
+
self.normal[idx] = normal
|
|
317
|
+
self.surface_id[idx] = surface_id
|
|
318
|
+
|
|
319
|
+
@ti.func
|
|
320
|
+
def get(self, idx: ti.i32):
|
|
321
|
+
return (self.hit[idx], self.t[idx], self.point[idx],
|
|
322
|
+
self.normal[idx], self.surface_id[idx])
|