Create main.py

This commit is contained in:
OusmBlueNinja 2025-04-10 11:12:35 -05:00
parent 5efa88485c
commit aa4e288c3d

299
voxel_grid_bvh/main.py Normal file
View File

@ -0,0 +1,299 @@
import pygame
import math
import sys
# ---------- Configuration ----------
WIDTH, HEIGHT = 800, 600
VOXEL_SIZE = 10
# ---------- Global Counters ----------
# These counters are used to sum the number of AABB intersection tests
# over all rays (reset each frame).
total_bvh_checks = 0
total_grid_checks = 0
# ---------- Data Structures ----------
class AABB:
"""Axis-Aligned Bounding Box for a voxel or a BVH node."""
def __init__(self, x, y, w, h):
self.x = x # top-left x
self.y = y # top-left y
self.w = w
self.h = h
def union(self, other):
"""Returns the smallest AABB that contains both self and other."""
x_min = min(self.x, other.x)
y_min = min(self.y, other.y)
x_max = max(self.x + self.w, other.x + other.w)
y_max = max(self.y + self.h, other.y + other.h)
return AABB(x_min, y_min, x_max - x_min, y_max - y_min)
def intersect_ray(self, origin, direction):
"""
Ray-AABB intersection using the slab method.
Returns distance t if hit, or None if no intersection.
"""
invDx = 1.0 / (direction[0] if direction[0] != 0 else 1e-6)
invDy = 1.0 / (direction[1] if direction[1] != 0 else 1e-6)
t1 = (self.x - origin[0]) * invDx
t2 = ((self.x + self.w) - origin[0]) * invDx
t3 = (self.y - origin[1]) * invDy
t4 = ((self.y + self.h) - origin[1]) * invDy
tmin = max(min(t1, t2), min(t3, t4))
tmax = min(max(t1, t2), max(t3, t4))
if tmax < 0 or tmin > tmax:
return None # No intersection
return tmin
class BVHNode:
"""A node in the BVH. If leaf, 'voxel' is set; otherwise, children are in 'left' and 'right'."""
def __init__(self, aabb, voxel=None, left=None, right=None):
self.aabb = aabb
self.voxel = voxel # (i, j) tuple if leaf
self.left = left
self.right = right
# Use red color for all nodes
self.color = (255, 0, 0)
def build_bvh(voxels):
"""
Build a BVH recursively from a list of voxels.
Each voxel is a tuple: (i, j, AABB)
"""
if not voxels:
return None
if len(voxels) == 1:
i, j, aabb = voxels[0]
return BVHNode(aabb, voxel=(i, j))
# Compute combined bounding box.
combined = voxels[0][2]
for _, _, aabb in voxels[1:]:
combined = combined.union(aabb)
# Choose the axis to split: 0 = x, 1 = y.
axis = 0
if combined.w < combined.h:
axis = 1
# Sort voxels by the center coordinate on the chosen axis.
voxels.sort(key=lambda item: (item[2].x + item[2].w / 2) if axis == 0 else (item[2].y + item[2].h / 2))
mid = len(voxels) // 2
left_node = build_bvh(voxels[:mid])
right_node = build_bvh(voxels[mid:])
return BVHNode(combined, left=left_node, right=right_node)
def bvh_raycast(node, ray_origin, ray_dir, counters):
"""
Traverse the BVH recursively and perform a raycast.
Only at leaf nodes will the actual voxel "collision" be returned.
The 'counters' dictionary tracks the number of intersection tests.
Returns a tuple (hit_voxel, t_hit) if a hit is found, else None.
"""
if node is None:
return None
counters['bvh'] += 1 # Count this node's AABB check
t = node.aabb.intersect_ray(ray_origin, ray_dir)
if t is None:
return None
# If leaf, return the voxel (no additional voxel-specific intersection is done)
if node.voxel is not None:
return (node.voxel, t)
hit_left = bvh_raycast(node.left, ray_origin, ray_dir, counters)
hit_right = bvh_raycast(node.right, ray_origin, ray_dir, counters)
if hit_left and hit_right:
return hit_left if hit_left[1] < hit_right[1] else hit_right
elif hit_left:
return hit_left
elif hit_right:
return hit_right
else:
return None
def grid_raycast(voxels, ray_origin, ray_dir, counters):
"""
Perform a brute-force raycast over all voxels in the grid.
For each voxel a ray-AABB test is done.
Returns a tuple (hit_voxel, t_hit) if a voxel is hit, else None.
"""
hit = None
for (i, j) in voxels.keys():
# Create the voxel's AABB.
aabb = AABB(i * VOXEL_SIZE, j * VOXEL_SIZE, VOXEL_SIZE, VOXEL_SIZE)
counters['grid'] += 1
t = aabb.intersect_ray(ray_origin, ray_dir)
if t is not None:
if hit is None or t < hit[1]:
hit = ((i, j), t)
return hit
def draw_bvh(node, surface):
"""Recursively draw the BVH bounding boxes in red."""
if node is None:
return
rect = pygame.Rect(node.aabb.x, node.aabb.y, node.aabb.w, node.aabb.h)
pygame.draw.rect(surface, node.color, rect, 1)
draw_bvh(node.left, surface)
draw_bvh(node.right, surface)
# ---------- Main Program ----------
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("2D Voxel Editor with BVH & Multi-Ray Debug")
clock = pygame.time.Clock()
font = pygame.font.SysFont('Arial', 16)
# Dictionary to store voxels: key is (i, j) grid coordinate.
voxels = {}
def add_voxel(i, j):
voxels[(i, j)] = True
def remove_voxel(i, j):
if (i, j) in voxels:
del voxels[(i, j)]
# Control flag for debug mode (toggled with F3).
debug_mode = True
running = True
mouse_down = False
mouse_button = None
while running:
# Reset the total counters each frame.
total_bvh_checks = 0
total_grid_checks = 0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_F3:
debug_mode = not debug_mode
elif event.type == pygame.MOUSEBUTTONDOWN:
mouse_down = True
mouse_button = event.button # 1=left, 3=right
elif event.type == pygame.MOUSEBUTTONUP:
mouse_down = False
mouse_button = None
# Process mouse dragging to add or remove voxels.
if mouse_down:
mx, my = pygame.mouse.get_pos()
grid_x = mx // VOXEL_SIZE
grid_y = my // VOXEL_SIZE
if mouse_button == 1:
add_voxel(grid_x, grid_y)
elif mouse_button == 3:
remove_voxel(grid_x, grid_y)
# Build a list of voxel AABBs for the BVH.
voxel_list = []
for (i, j) in voxels.keys():
aabb = AABB(i * VOXEL_SIZE, j * VOXEL_SIZE, VOXEL_SIZE, VOXEL_SIZE)
voxel_list.append((i, j, aabb))
bvh_root = build_bvh(voxel_list)
# Set up multi-ray casting.
center = (WIDTH / 2, HEIGHT / 2)
mouse_pos = pygame.mouse.get_pos()
# Compute base direction from center to mouse pointer.
base_angle = math.atan2(mouse_pos[1] - center[1], mouse_pos[0] - center[0])
num_rays = 25
fov_degrees = 25 # total field-of-view in degrees.
half_fov = math.radians(fov_degrees / 2)
angle_step = math.radians(fov_degrees) / (num_rays - 1)
# Prepare lists to store results for each ray.
ray_lines = [] # (start, end) of each ray.
bvh_ray_hits = [] # hit voxel from BVH per ray.
grid_ray_hits = [] # hit voxel from grid per ray.
ray_counters = [] # each element is a tuple (bvh_checks, grid_checks) for that ray.
for i in range(num_rays):
# Calculate the angle for this ray.
angle = base_angle - half_fov + i * angle_step
ray_dir = (math.cos(angle), math.sin(angle))
# Define a distant end point (for drawing the ray).
ray_length = max(WIDTH, HEIGHT)
ray_end = (center[0] + ray_dir[0] * ray_length,
center[1] + ray_dir[1] * ray_length)
# Prepare separate counters for this ray.
counters = {'bvh': 0, 'grid': 0}
# Perform BVH raycast.
bvh_result = bvh_raycast(bvh_root, center, ray_dir, counters)
# Perform brute-force grid raycast.
grid_result = grid_raycast(voxels, center, ray_dir, counters)
ray_lines.append((center, ray_end))
bvh_ray_hits.append(bvh_result)
grid_ray_hits.append(grid_result)
ray_counters.append((counters['bvh'], counters['grid']))
total_bvh_checks += counters['bvh']
total_grid_checks += counters['grid']
# ---------- Rendering ----------
screen.fill((30, 30, 30))
# Draw voxels as filled white squares.
for (i, j) in voxels.keys():
rect = pygame.Rect(i * VOXEL_SIZE, j * VOXEL_SIZE, VOXEL_SIZE, VOXEL_SIZE)
pygame.draw.rect(screen, (255, 255, 255), rect)
if debug_mode:
# Draw the BVH bounding boxes in red.
if bvh_root:
draw_bvh(bvh_root, screen)
# Draw each ray (green lines).
for ray_line in ray_lines:
pygame.draw.line(screen, (0, 255, 0), ray_line[0], ray_line[1], 1)
# For each ray, if it hit a voxel via the BVH, highlight it with a blue border.
for hit in bvh_ray_hits:
if hit:
hit_voxel, _ = hit
i, j = hit_voxel
hit_rect = pygame.Rect(i * VOXEL_SIZE, j * VOXEL_SIZE, VOXEL_SIZE, VOXEL_SIZE)
pygame.draw.rect(screen, (0, 0, 255), hit_rect, 3)
# Display debug menu with ray results and counters.
debug_text = [
"DEBUG MODE (F3 toggles)",
"Left-drag: add voxel, Right-drag: remove voxel",
"25 Rays from center in 25° FOV (green)",
f"Total BVH Intersection Checks: {total_bvh_checks}",
f"Total Grid Intersection Checks: {total_grid_checks}"
]
# Optionally add per-ray info (commented out for clarity)
for idx, (bvh_count, grid_count) in enumerate(ray_counters):
debug_text.append(f"Ray {idx}: BVH={bvh_count} Grid={grid_count}")
for idx, line in enumerate(debug_text):
text_surf = font.render(line, True, (255, 255, 0))
screen.blit(text_surf, (5, 5 + idx * 18))
pygame.display.flip()
clock.tick()
pygame.quit()
sys.exit()