Skip to content

Commit f2a6f92

Browse files
committed
Add Numba-accelerated mesh generation and BVH construction; enhance GPU rendering in test
1 parent d4ad0de commit f2a6f92

4 files changed

Lines changed: 452 additions & 3 deletions

File tree

src/voxcity/geoprocessor/mesh.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,174 @@
1515
import matplotlib.pyplot as plt
1616
from ..utils.orientation import ensure_orientation, ORIENTATION_NORTH_UP, ORIENTATION_SOUTH_UP
1717

18+
# Try to import numba for accelerated mesh generation
19+
_HAS_NUMBA = False
20+
try:
21+
from numba import njit, prange
22+
_HAS_NUMBA = True
23+
except ImportError:
24+
pass
25+
26+
27+
# ============================================================================
28+
# Numba-accelerated voxel mesh generation
29+
# ============================================================================
30+
31+
if _HAS_NUMBA:
32+
@njit(cache=True)
33+
def _generate_mesh_data_numba_serial(
34+
voxel_mask: np.ndarray,
35+
meshsize: float,
36+
):
37+
"""
38+
Generate mesh vertices, faces and normals for visible voxel faces.
39+
Serial version that returns arrays directly.
40+
"""
41+
nx, ny, nz = voxel_mask.shape
42+
43+
# First pass: count visible faces
44+
n_faces = 0
45+
for x in range(nx):
46+
for y in range(ny):
47+
for z in range(nz):
48+
if not voxel_mask[x, y, z]:
49+
continue
50+
if z + 1 >= nz or not voxel_mask[x, y, z + 1]:
51+
n_faces += 1
52+
if z - 1 < 0 or not voxel_mask[x, y, z - 1]:
53+
n_faces += 1
54+
if x + 1 >= nx or not voxel_mask[x + 1, y, z]:
55+
n_faces += 1
56+
if x - 1 < 0 or not voxel_mask[x - 1, y, z]:
57+
n_faces += 1
58+
if y + 1 >= ny or not voxel_mask[x, y + 1, z]:
59+
n_faces += 1
60+
if y - 1 < 0 or not voxel_mask[x, y - 1, z]:
61+
n_faces += 1
62+
63+
if n_faces == 0:
64+
return np.zeros((0, 3), dtype=np.float32), np.zeros((0, 3), dtype=np.int32), np.zeros((0, 3), dtype=np.float32)
65+
66+
# Allocate output arrays
67+
vertices = np.zeros((n_faces * 4, 3), dtype=np.float32)
68+
faces = np.zeros((n_faces * 2, 3), dtype=np.int32)
69+
normals = np.zeros((n_faces * 2, 3), dtype=np.float32)
70+
71+
# Unit cube face definitions (4 vertices per face, as quads)
72+
unit_faces = np.array([
73+
# Front (+Z)
74+
[[0.0, 0.0, 1.0], [1.0, 0.0, 1.0], [1.0, 1.0, 1.0], [0.0, 1.0, 1.0]],
75+
# Back (-Z)
76+
[[0.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0], [1.0, 0.0, 0.0]],
77+
# Right (+X)
78+
[[1.0, 0.0, 0.0], [1.0, 1.0, 0.0], [1.0, 1.0, 1.0], [1.0, 0.0, 1.0]],
79+
# Left (-X)
80+
[[0.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 1.0, 0.0]],
81+
# Top (+Y)
82+
[[0.0, 1.0, 0.0], [0.0, 1.0, 1.0], [1.0, 1.0, 1.0], [1.0, 1.0, 0.0]],
83+
# Bottom (-Y)
84+
[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [1.0, 0.0, 1.0], [0.0, 0.0, 1.0]]
85+
], dtype=np.float32)
86+
87+
face_normals_ref = np.array([
88+
[0.0, 0.0, 1.0], # Front
89+
[0.0, 0.0, -1.0], # Back
90+
[1.0, 0.0, 0.0], # Right
91+
[-1.0, 0.0, 0.0], # Left
92+
[0.0, 1.0, 0.0], # Top
93+
[0.0, -1.0, 0.0] # Bottom
94+
], dtype=np.float32)
95+
96+
# Second pass: generate mesh data
97+
face_idx = 0
98+
for x in range(nx):
99+
for y in range(ny):
100+
for z in range(nz):
101+
if not voxel_mask[x, y, z]:
102+
continue
103+
104+
px = np.float32(x)
105+
py = np.float32(y)
106+
pz = np.float32(z)
107+
108+
for dir_idx in range(6):
109+
is_visible = False
110+
if dir_idx == 0: # +Z
111+
is_visible = z + 1 >= nz or not voxel_mask[x, y, z + 1]
112+
elif dir_idx == 1: # -Z
113+
is_visible = z - 1 < 0 or not voxel_mask[x, y, z - 1]
114+
elif dir_idx == 2: # +X
115+
is_visible = x + 1 >= nx or not voxel_mask[x + 1, y, z]
116+
elif dir_idx == 3: # -X
117+
is_visible = x - 1 < 0 or not voxel_mask[x - 1, y, z]
118+
elif dir_idx == 4: # +Y
119+
is_visible = y + 1 >= ny or not voxel_mask[x, y + 1, z]
120+
elif dir_idx == 5: # -Y
121+
is_visible = y - 1 < 0 or not voxel_mask[x, y - 1, z]
122+
123+
if is_visible:
124+
vert_base = face_idx * 4
125+
tri_base = face_idx * 2
126+
127+
# Generate 4 vertices for this quad face
128+
for v in range(4):
129+
vertices[vert_base + v, 0] = (unit_faces[dir_idx, v, 0] + px) * meshsize
130+
vertices[vert_base + v, 1] = (unit_faces[dir_idx, v, 1] + py) * meshsize
131+
vertices[vert_base + v, 2] = (unit_faces[dir_idx, v, 2] + pz) * meshsize
132+
133+
# Generate 2 triangles
134+
faces[tri_base, 0] = vert_base
135+
faces[tri_base, 1] = vert_base + 1
136+
faces[tri_base, 2] = vert_base + 2
137+
faces[tri_base + 1, 0] = vert_base
138+
faces[tri_base + 1, 1] = vert_base + 2
139+
faces[tri_base + 1, 2] = vert_base + 3
140+
141+
# Set normals for both triangles
142+
normals[tri_base, 0] = face_normals_ref[dir_idx, 0]
143+
normals[tri_base, 1] = face_normals_ref[dir_idx, 1]
144+
normals[tri_base, 2] = face_normals_ref[dir_idx, 2]
145+
normals[tri_base + 1, 0] = face_normals_ref[dir_idx, 0]
146+
normals[tri_base + 1, 1] = face_normals_ref[dir_idx, 1]
147+
normals[tri_base + 1, 2] = face_normals_ref[dir_idx, 2]
148+
149+
face_idx += 1
150+
151+
return vertices, faces, normals
152+
153+
154+
def create_voxel_mesh_fast(voxel_array, class_id, meshsize=1.0):
155+
"""
156+
Fast voxel mesh generation using numba JIT compilation.
157+
158+
This is significantly faster than the original create_voxel_mesh for large arrays.
159+
"""
160+
# Create boolean mask for the target class
161+
voxel_mask = (voxel_array == class_id)
162+
163+
if not np.any(voxel_mask):
164+
return None
165+
166+
# Generate mesh data using numba
167+
vertices, faces, normals = _generate_mesh_data_numba_serial(voxel_mask, float(meshsize))
168+
169+
if len(faces) == 0:
170+
return None
171+
172+
# Create trimesh
173+
mesh = trimesh.Trimesh(
174+
vertices=vertices,
175+
faces=faces,
176+
face_normals=normals,
177+
process=False # Skip processing for speed
178+
)
179+
180+
# Merge duplicate vertices
181+
mesh.merge_vertices()
182+
183+
return mesh
184+
185+
18186
def create_voxel_mesh(voxel_array, class_id, meshsize=1.0, building_id_grid=None, mesh_type=None):
19187
"""
20188
Create a 3D mesh from voxels preserving sharp edges, scaled by meshsize.
@@ -87,6 +255,11 @@ def create_voxel_mesh(voxel_array, class_id, meshsize=1.0, building_id_grid=None
87255
- For buildings (class_id=-3), building IDs are tracked to maintain building identity.
88256
- The mesh preserves sharp edges, which is important for architectural visualization.
89257
"""
258+
# Use fast numba implementation when available and no special features needed
259+
if _HAS_NUMBA and building_id_grid is None and mesh_type is None:
260+
return create_voxel_mesh_fast(voxel_array, class_id, meshsize)
261+
262+
# Fall back to original implementation for special cases
90263
# Find voxels of the current class
91264
voxel_coords = np.argwhere(voxel_array == class_id)
92265

src/voxcity/visualizer/palette.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ def get_voxel_color_map(color_scheme='default'):
2121
-13: [186, 187, 181],
2222
-12: [248, 166, 2],
2323
-11: [81, 59, 56],
24+
-5: [102, 89, 76], # City furniture (urban brown)
25+
-4: [69, 72, 97], # Bridge (road-like dark gray-blue)
2426
-3: [180, 187, 216],
2527
-2: [78, 99, 63],
2628
-1: [188, 143, 143],
@@ -50,6 +52,8 @@ def get_voxel_color_map(color_scheme='default'):
5052
-13: [128, 128, 128],
5153
-12: [255, 128, 0],
5254
-11: [153, 0, 0],
55+
-5: [153, 102, 51], # City furniture (brown)
56+
-4: [25, 25, 25], # Bridge (road-like dark)
5357
-3: [0, 255, 255],
5458
-2: [0, 153, 0],
5559
-1: [204, 0, 102],
@@ -79,6 +83,8 @@ def get_voxel_color_map(color_scheme='default'):
7983
-13: [204, 204, 230],
8084
-12: [76, 76, 178],
8185
-11: [25, 25, 127],
86+
-5: [89, 89, 140], # City furniture
87+
-4: [61, 61, 137], # Bridge (road-like)
8288
-3: [179, 179, 230],
8389
-2: [51, 51, 153],
8490
-1: [102, 102, 178],
@@ -108,6 +114,8 @@ def get_voxel_color_map(color_scheme='default'):
108114
-13: [226, 226, 226],
109115
-12: [255, 223, 179],
110116
-11: [204, 168, 166],
117+
-5: [209, 196, 186], # City furniture (warm beige)
118+
-4: [199, 200, 214], # Bridge (road-like lavender gray)
111119
-3: [214, 217, 235],
112120
-2: [190, 207, 180],
113121
-1: [235, 204, 204],
@@ -137,6 +145,8 @@ def get_voxel_color_map(color_scheme='default'):
137145
-13: [61, 61, 61],
138146
-12: [153, 102, 0],
139147
-11: [51, 35, 33],
148+
-5: [61, 53, 46], # City furniture (dark brown)
149+
-4: [35, 41, 53], # Bridge (road-like dark)
140150
-3: [78, 82, 99],
141151
-2: [46, 58, 37],
142152
-1: [99, 68, 68],
@@ -166,6 +176,8 @@ def get_voxel_color_map(color_scheme='default'):
166176
-13: [180, 180, 180],
167177
-12: [170, 170, 170],
168178
-11: [70, 70, 70],
179+
-5: [80, 80, 80], # City furniture
180+
-4: [40, 40, 40], # Bridge (road-like)
169181
-3: [190, 190, 190],
170182
-2: [90, 90, 90],
171183
-1: [160, 160, 160],
@@ -195,6 +207,8 @@ def get_voxel_color_map(color_scheme='default'):
195207
-13: [236, 236, 236],
196208
-12: [245, 232, 210],
197209
-11: [235, 210, 205],
210+
-5: [228, 220, 212], # City furniture (warm white)
211+
-4: [215, 215, 220], # Bridge (road-like gray)
198212
-3: [225, 230, 240],
199213
-2: [190, 210, 190],
200214
-1: [230, 215, 215],

0 commit comments

Comments
 (0)