Skip to content

Commit 2dbbafc

Browse files
authored
Merge pull request #246 from joshuacortez/feat/voxel_traversal_improvement
update voxel traversal algo using only integer arithmetic
2 parents 61e2fca + fe4519b commit 2dbbafc

File tree

3 files changed

+120
-282
lines changed

3 files changed

+120
-282
lines changed

geowrangler/gridding_utils/polygon_fill.py

Lines changed: 24 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ def voxel_traversal_2d(
1616
end_vertex: Tuple[int, int],
1717
debug: bool = False, # if true, prints diagnostic info for the algorithm
1818
) -> List[Tuple[int, int]]:
19-
"""Returns all pixels between two points as inspired by Amanatides & Woo's “A Fast Voxel Traversal Algorithm For Ray Tracing”"""
20-
21-
# epsilon is a constant for correcting near-misses in voxel traversal
22-
EPSILON = 1e-14
19+
"""
20+
Returns all pixels between two points as inspired by Amanatides & Woo's “A Fast Voxel Traversal Algorithm For Ray Tracing”
21+
Implementation adapted from https://www.redblobgames.com/grids/line-drawing/
22+
"""
2323

2424
# Setup initial conditions
2525
x1, y1 = start_vertex
@@ -45,31 +45,21 @@ def voxel_traversal_2d(
4545

4646
dy = y2 - y1
4747
dx = x2 - x1
48-
slope = dy / dx
49-
inv_slope = dx / dy
50-
51-
# reverse order if negative slope to preserve symmetry in floating point calculations
52-
if slope < 0:
53-
x1, y1 = end_vertex
54-
x2, y2 = start_vertex
55-
56-
direction_x = 1 if x2 > x1 else -1
57-
direction_y = 1 if y2 > y1 else -1
58-
59-
slope_multiplier = np.sqrt(1 + slope**2)
60-
inv_slope_multiplier = np.sqrt(1 + inv_slope**2)
6148

6249
pixel_x, pixel_y = x1, y1
63-
ray_x, ray_y = pixel_x, pixel_y
6450
pixels = [(pixel_x, pixel_y)]
6551

6652
is_finished = False
6753

6854
if debug:
6955
print(f"\nTraversing from ({x1},{y1}) to ({x2},{y2})")
7056

71-
# number of steps should not exceed the perimeter of the rectangle enclosing the line segment
72-
max_steps = 2 * (abs(dx) + abs(dy))
57+
ix = 0
58+
iy = 0
59+
60+
nx = abs(dx)
61+
ny = abs(dy)
62+
max_steps = nx + ny
7363
n_steps = 0
7464
while not is_finished:
7565
# this prevents infinite loops
@@ -79,71 +69,27 @@ def voxel_traversal_2d(
7969
f"Traversal has exceeded steps limit {max_steps:,}. Please recheck inputs"
8070
)
8171

82-
# get the next x or y integer that the next ray would hit
83-
if direction_x == 1:
84-
next_ray_x = np.floor(ray_x) + 1
85-
elif direction_x == -1:
86-
next_ray_x = np.ceil(ray_x) - 1
87-
88-
if direction_y == 1:
89-
next_ray_y = np.floor(ray_y) + 1
90-
elif direction_y == -1:
91-
next_ray_y = np.ceil(ray_y) - 1
92-
93-
# get distance between the 2 candidates and check which one is closer
94-
# there is an epsilon to account near-misses due to floating point differences
95-
96-
# y coordinate line formula is next_ray_y = ray_y + slope*(next_ray_x-ray_x)
97-
# squred distance is (next_ray_x - ray_x)**2 + (slope*(next_ray_x-ray_x))**2
98-
# distance simplifies to abs(next_ray_x - ray_x)* sqrt(1+slope**2)
99-
100-
ray_candidate_1 = (
101-
next_ray_x,
102-
ray_y + slope * (next_ray_x - ray_x) + direction_y * EPSILON,
103-
)
104-
# unsimplified square distance
105-
# dist_1 = (ray_candidate_1[0] - ray_x)**2 + (ray_candidate_1[1] - ray_y)**2
106-
# simplified distance
107-
dist_1 = abs(next_ray_x - ray_x) * slope_multiplier
108-
109-
# x coordinate line formula is next_ray_x = ray_x + inv_slope*(next_ray_y-y)
110-
# squared distance is (inv_slope*(next_ray_y-ray_y))**2 + (next_ray_y-ray_y)**2
111-
# distance simplifies to abs(next_ray_y-ray_y)* sqrt(1 + inv_slope**2)
112-
113-
ray_candidate_2 = (
114-
ray_x + inv_slope * (next_ray_y - ray_y) + direction_x * EPSILON,
115-
next_ray_y,
116-
)
117-
# unsimplified square distance
118-
# dist_2 = (ray_candidate_2[0] - ray_x)**2 + (ray_candidate_2[1] - ray_y)**2
119-
# simplified distance
120-
dist_2 = abs(next_ray_y - ray_y) * inv_slope_multiplier
121-
122-
# candidate 1 is closer
123-
if dist_1 < dist_2:
72+
decision = (1 + 2 * ix) * ny - (1 + 2 * iy) * nx
73+
if decision == 0:
74+
# diagonal step
12475
pixel_x += direction_x
125-
ray_x, ray_y = ray_candidate_1
126-
127-
# candidate 2 is closer
128-
elif dist_1 > dist_2:
12976
pixel_y += direction_y
130-
ray_x, ray_y = ray_candidate_2
131-
132-
# line passes exactly on the corner
133-
elif dist_1 == dist_2:
77+
ix += 1
78+
iy += 1
79+
elif decision < 0:
80+
# horizontal step
13481
pixel_x += direction_x
135-
pixel_y += direction_y
136-
ray_x, ray_y = pixel_x, pixel_y
82+
ix += 1
13783
else:
138-
raise ValueError(f"Erroneous distances {dist_1}, {dist_2}")
139-
140-
if debug:
141-
print(
142-
f"Next ray coords are ({ray_x}, {ray_y}) and tile coords are ({pixel_x}, {pixel_y})"
143-
)
84+
# vetical step
85+
pixel_y += direction_y
86+
iy += 1
14487

14588
pixels.append((pixel_x, pixel_y))
14689

90+
if debug:
91+
print(f"Next tile coords are ({pixel_x}, {pixel_y})")
92+
14793
# checks to see if the loop is finished
14894
if direction_x == 1:
14995
is_x_finished = pixel_x >= x2

0 commit comments

Comments
 (0)