Skip to content

Commit 9edfad4

Browse files
committed
Add USD normals importer with hierarchy and BSP support
1 parent fd8179a commit 9edfad4

22 files changed

Lines changed: 2313 additions & 0 deletions
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import numpy as np
2+
3+
class BSPNode:
4+
"""
5+
Node of a Binary Space Partitioning (BSP) tree.
6+
"""
7+
8+
def __init__(self, axis=None, value=None):
9+
self.left = None
10+
self.right = None
11+
self.axis = axis
12+
self.value = value
13+
self.triangles = None # np.ndarray of triangle ids stored in leaves
14+
15+
def isLeaf(self):
16+
"""
17+
Check whether this node is a leaf.
18+
"""
19+
return self.left is None and self.right is None
20+
21+
22+
class BSPTree:
23+
"""
24+
Binary Space Partitioning (BSP) tree for triangle meshes.
25+
"""
26+
27+
def __init__(self, vertices: np.ndarray, indices: np.ndarray, max_depth: int = 18):
28+
self.vertices = vertices.astype(np.float32)
29+
self.indices = indices
30+
self.max_depth = max_depth
31+
self.root = None
32+
self.triangle_ids = None
33+
34+
def build(self):
35+
"""
36+
Build the BSP tree from the provided vertices and indices.
37+
"""
38+
# Reshape indices to (T, 3)
39+
self.indices = self.indices.reshape(-1, 3).astype(np.int32)
40+
T = self.indices.shape[0]
41+
self.triangle_ids = np.arange(T, dtype=np.int32)
42+
43+
# Create root node and start recursive splitting
44+
self.root = BSPNode()
45+
empty = np.array([], dtype=np.int32)
46+
self.split(self.root, self.triangle_ids, 0, empty)
47+
48+
def tri_axis_minmax(self, tri_ids: np.ndarray, axis: int):
49+
"""
50+
Compute per-triangle min and max coordinates along a given axis.
51+
"""
52+
tris = self.indices[tri_ids]
53+
v = self.vertices[tris]
54+
a = v[:, :, axis]
55+
return a.min(axis=1), a.max(axis=1)
56+
57+
def choose_axis_value(self, tri_ids: np.ndarray):
58+
"""
59+
Choose the splitting axis and split value for a set of triangles.
60+
"""
61+
tris = self.indices[tri_ids]
62+
v = self.vertices[tris]
63+
tri_min = v.min(axis=1)
64+
tri_max = v.max(axis=1)
65+
66+
overall_min = tri_min.min(axis=0)
67+
overall_max = tri_max.max(axis=0)
68+
spreads = overall_max - overall_min
69+
70+
# Try axes in descending spread order
71+
for axis in np.argsort(spreads)[::-1]:
72+
axis = int(axis)
73+
if spreads[axis] < 1e-9:
74+
continue
75+
76+
centroids = v.mean(axis=1)
77+
value = float(np.median(centroids[:, axis]))
78+
79+
minA = tri_min[:, axis]
80+
maxA = tri_max[:, axis]
81+
82+
left_possible = np.any(maxA < value)
83+
right_possible = np.any(minA > value)
84+
85+
if left_possible and right_possible:
86+
return axis, value
87+
88+
return None, None
89+
90+
def split(self, node: BSPNode, tri_ids: np.ndarray, depth: int, carry_ids: np.ndarray):
91+
"""
92+
Recursively split triangles into a BSP tree.
93+
94+
Triangles that intersect the split plane are not passed to children;
95+
instead, they are inherited by all descendant leaves via the carry_ids
96+
mechanism.
97+
"""
98+
n = int(tri_ids.size)
99+
100+
# Stop criteria: create a leaf storing inherited and remaining triangles
101+
if depth >= self.max_depth or n == 0:
102+
if carry_ids.size == 0:
103+
node.triangles = tri_ids.astype(np.int32)
104+
elif n == 0:
105+
node.triangles = carry_ids.astype(np.int32)
106+
else:
107+
node.triangles = np.unique(
108+
np.concatenate([carry_ids.astype(np.int32), tri_ids.astype(np.int32)])).astype(np.int32)
109+
return
110+
111+
axis, value = self.choose_axis_value(tri_ids)
112+
if axis is None:
113+
if carry_ids.size == 0:
114+
node.triangles = tri_ids.astype(np.int32)
115+
else:
116+
node.triangles = np.unique(np.concatenate([carry_ids.astype(np.int32), tri_ids.astype(np.int32)])).astype(np.int32)
117+
return
118+
119+
minA, maxA = self.tri_axis_minmax(tri_ids, axis)
120+
121+
left_only = []
122+
right_only = []
123+
intersected = [] # both left and right
124+
125+
for i, tid in enumerate(tri_ids):
126+
if maxA[i] < value:
127+
left_only.append(int(tid))
128+
elif minA[i] > value:
129+
right_only.append(int(tid))
130+
else:
131+
intersected.append(int(tid))
132+
133+
left_ids = np.array(left_only, dtype=np.int32)
134+
right_ids = np.array(right_only, dtype=np.int32)
135+
stay_ids = np.array(intersected, dtype=np.int32)
136+
137+
# If the split is not meaningful, create a leaf
138+
if left_ids.size == 0 or right_ids.size == 0:
139+
if carry_ids.size == 0:
140+
node.triangles = tri_ids.astype(np.int32)
141+
else:
142+
node.triangles = np.unique(
143+
np.concatenate([carry_ids.astype(np.int32), tri_ids.astype(np.int32)])).astype(np.int32)
144+
return
145+
146+
# Compute new inherited triangles for children
147+
if carry_ids.size == 0:
148+
new_carry = stay_ids
149+
elif stay_ids.size == 0:
150+
new_carry = carry_ids.astype(np.int32)
151+
else:
152+
new_carry = np.unique(np.concatenate([carry_ids.astype(np.int32), stay_ids])).astype(np.int32)
153+
154+
# Commit internal node
155+
node.axis = axis
156+
node.value = float(value)
157+
node.triangles = None
158+
159+
node.left = BSPNode()
160+
node.right = BSPNode()
161+
162+
self.split(node.left, left_ids, depth + 1, new_carry)
163+
self.split(node.right, right_ids, depth + 1, new_carry)
164+
165+
def trianglesCentroids(self):
166+
# self.indices: (T,3)
167+
i0 = self.indices[:, 0]
168+
i1 = self.indices[:, 1]
169+
i2 = self.indices[:, 2]
170+
171+
v0 = self.vertices[i0]
172+
v1 = self.vertices[i1]
173+
v2 = self.vertices[i2]
174+
175+
centroids = (v0 + v1 + v2) / 3.0
176+
return centroids.astype(np.float32)
177+
178+
def print_by_depth(self):
179+
if self.root is None:
180+
print("Empty tree")
181+
return
182+
183+
q = [(self.root, 0)]
184+
idx = 0
185+
cur_depth = 0
186+
print("Depth 0:")
187+
188+
while idx < len(q):
189+
node, d = q[idx]
190+
idx += 1
191+
192+
if d != cur_depth:
193+
cur_depth = d
194+
print(f"\nDepth {cur_depth}:")
195+
196+
if node.isLeaf():
197+
print(f" Leaf(tris={len(node.triangles)})", end=" ")
198+
else:
199+
print(f" (a={node.axis}, v={node.value:.2f})", end=" ")
200+
if node.left is not None:
201+
q.append((node.left, d + 1))
202+
if node.right is not None:
203+
q.append((node.right, d + 1))
204+
205+
print()
206+
207+
def search(self, tri_id: int):
208+
"""
209+
Directed BSP traversal.
210+
Returns a list of (leaf_node, path) where path contains
211+
(axis, value, decision) entries.
212+
"""
213+
tri_id = int(tri_id)
214+
if self.root is None:
215+
return []
216+
217+
# Compute triangle bounds once
218+
tri = self.indices[tri_id]
219+
v = self.vertices[tri]
220+
tri_min = v.min(axis=0)
221+
tri_max = v.max(axis=0)
222+
223+
results = []
224+
stack = [(self.root, [])] # (node, path)
225+
226+
while stack:
227+
node, path = stack.pop()
228+
if node is None:
229+
continue
230+
231+
if node.isLeaf():
232+
if node.triangles is not None and tri_id in node.triangles:
233+
results.append((node, path))
234+
continue
235+
236+
a = node.axis
237+
s = node.value
238+
239+
if tri_max[a] < s:
240+
# LEFT only
241+
new_path = path + [(a, s, "LEFT")]
242+
stack.append((node.left, new_path))
243+
244+
elif tri_min[a] > s:
245+
# RIGHT only
246+
new_path = path + [(a, s, "RIGHT")]
247+
stack.append((node.right, new_path))
248+
249+
else:
250+
# BOTH sides
251+
new_path_left = path + [(a, s, "BOTH")]
252+
new_path_right = path + [(a, s, "BOTH")]
253+
stack.append((node.left, new_path_left))
254+
stack.append((node.right, new_path_right))
255+
256+
for i, (_, path) in enumerate(results):
257+
print(f"Path {i}:")
258+
for depth, (axis, value, decision) in enumerate(path):
259+
axis_name = ['x', 'y', 'z'][axis]
260+
print(f" Depth {depth}: split {axis_name} = {value:.2f} -> {decision}")
261+
print()
262+
263+
return results
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
Authors:
2+
Almani Iosif - csd4824
3+
Kapetanakis Ioannis - csd4641
4+
5+
Tests are written for pytest
6+
-> To run all the tests, type "pytest Tests"
7+
8+
<For Proper Normals Task:>
9+
Implemented correct flat/smooth normal handling by detecting shared vs unique vertex indexing and converting the vertex/index buffers accordingly. Custom helper functions exist to determine whether vertices are shared or unique by analyzing index usage. This correction fixes smooth-shading artifacts caused by incorrect vertex-sharing assumptions and ensures the appropriate flat or smooth shading path is selected.
10+
11+
Usage:
12+
python cow_example.py --shading {smooth|flat} [-colored]
13+
python sphere.py --shading {smooth|flat} [-colored]
14+
15+
Options:
16+
--shading Shading mode to use.
17+
smooth : smooth (per-vertex) normals
18+
flat : flat (per-face) normals
19+
20+
Flags:
21+
-colored Enable color visualization by using normals as colors (optional)
22+
23+
Example:
24+
python cow_example.py --shading flat -colored
25+
26+
27+
<For USD Importer Task:>
28+
The LoadScene_Blender method imports a USD scene exported from Blender and converts it into an Elements scene representation. It traverses the USD stage hierarchy, creates corresponding entities for each UsdGeom.Xform, and reconstructs parent–child relationships based on USD paths.
29+
30+
For each UsdGeom.Mesh, the method extracts geometry data including vertex positions, face topology, normals, and material color information. It supports both smooth and flat shading by handling different normal interpolation modes (vertex and faceVarying). Polygonal faces are triangulated appropriately, with corner-space triangulation used for flat shading.
31+
32+
The method uploads vertex attributes and indices to the GPU and performs coordinate system conversion from Blender’s Z-up convention to the engine’s Y-up convention.
33+
34+
Usage:
35+
python usd_import_example.py [-colored]
36+
37+
Flags:
38+
-colored Enable color visualization by using normals as colors (optional)
39+
40+
41+
<For BSP Task:>
42+
The implemented method builds an axis-aligned Binary Space Partitioning (BSP) tree for triangle meshes.
43+
44+
At each node, the splitting axis is selected based on the largest spatial extent of the triangles, while the split position is chosen as the median of triangle centroids along that axis to avoid unbalanced partitions.
45+
46+
Triangles are classified as fully on one side of the split plane or intersecting it; intersecting triangles are propagated to all child leaf nodes to preserve spatial correctness.
47+
48+
During search, the BSP tree is traversed by testing each triangle against the split planes. Depending on whether the triangle lies on one side of the plane or intersects it, the traversal proceeds to the appropriate child nodes.
49+
50+
Usage:
51+
python bsp_example.py

0 commit comments

Comments
 (0)