ray-cast stuff
This commit is contained in:
parent
aa10b7f249
commit
75a6a3c706
115
bounding-box-thing/bvh.py
Normal file
115
bounding-box-thing/bvh.py
Normal file
@ -0,0 +1,115 @@
|
||||
import pygame
|
||||
import sys
|
||||
import random
|
||||
|
||||
# Initialize Pygame and set up the window
|
||||
pygame.init()
|
||||
screen = pygame.display.set_mode((800, 600))
|
||||
pygame.display.set_caption("Simple 2D BVH Visualization")
|
||||
clock = pygame.time.Clock()
|
||||
font = pygame.font.Font(None, 24)
|
||||
|
||||
# Utility functions to work with bounding boxes.
|
||||
# We represent a bbox as (min_x, min_y, max_x, max_y).
|
||||
def rect_to_bbox(rect):
|
||||
x, y, w, h = rect
|
||||
return (x, y, x + w, y + h)
|
||||
|
||||
def union_bbox(bbox1, bbox2):
|
||||
x1 = min(bbox1[0], bbox2[0])
|
||||
y1 = min(bbox1[1], bbox2[1])
|
||||
x2 = max(bbox1[2], bbox2[2])
|
||||
y2 = max(bbox1[3], bbox2[3])
|
||||
return (x1, y1, x2, y2)
|
||||
|
||||
# BVH Node class
|
||||
class BVHNode:
|
||||
def __init__(self, bbox, left=None, right=None, obj=None):
|
||||
self.bbox = bbox # Bounding box: (min_x, min_y, max_x, max_y)
|
||||
self.left = left # Left child (BVHNode or None)
|
||||
self.right = right # Right child (BVHNode or None)
|
||||
self.obj = obj # For leaf nodes, store the actual rectangle
|
||||
|
||||
# Recursive BVH build function.
|
||||
# If there's one object, return a leaf node.
|
||||
# Otherwise, sort the objects by their center x-coordinate, split in half,
|
||||
# build left/right children, and compute the union of their bounding boxes.
|
||||
def build_bvh(objects):
|
||||
if not objects:
|
||||
return None
|
||||
if len(objects) == 1:
|
||||
return BVHNode(rect_to_bbox(objects[0]), obj=objects[0])
|
||||
|
||||
# Sort objects by center x-coordinate
|
||||
objects.sort(key=lambda rect: rect[0] + rect[2] / 2)
|
||||
mid = len(objects) // 2
|
||||
left = build_bvh(objects[:mid])
|
||||
right = build_bvh(objects[mid:])
|
||||
node_bbox = union_bbox(left.bbox, right.bbox)
|
||||
return BVHNode(node_bbox, left, right)
|
||||
|
||||
# Generate a list of random rectangles.
|
||||
def generate_rectangles(num_rects):
|
||||
rects = []
|
||||
for _ in range(num_rects):
|
||||
x = random.randint(50, 750)
|
||||
y = random.randint(50, 550)
|
||||
w = random.randint(20, 100)
|
||||
h = random.randint(20, 100)
|
||||
rects.append((x, y, w, h))
|
||||
return rects
|
||||
|
||||
num_rects = 20
|
||||
rectangles = generate_rectangles(num_rects)
|
||||
bvh_root = build_bvh(rectangles)
|
||||
|
||||
# Toggle for showing the BVH overlay
|
||||
show_bvh = True
|
||||
|
||||
# Recursively draw BVH nodes.
|
||||
def draw_bvh(node, surface):
|
||||
if node is None:
|
||||
return
|
||||
# Convert bbox from (min_x, min_y, max_x, max_y) to pygame.Rect
|
||||
x, y, x2, y2 = node.bbox
|
||||
rect = pygame.Rect(x, y, x2 - x, y2 - y)
|
||||
# Use green for leaf nodes and red for internal nodes.
|
||||
color = (0, 255, 0) if node.obj is not None else (255, 0, 0)
|
||||
pygame.draw.rect(surface, color, rect, 1)
|
||||
draw_bvh(node.left, surface)
|
||||
draw_bvh(node.right, surface)
|
||||
|
||||
# Main loop.
|
||||
running = True
|
||||
while running:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
# Toggle BVH display with D
|
||||
if event.key == pygame.K_d:
|
||||
show_bvh = not show_bvh
|
||||
# Regenerate rectangles and rebuild BVH with R
|
||||
elif event.key == pygame.K_r:
|
||||
rectangles = generate_rectangles(num_rects)
|
||||
bvh_root = build_bvh(rectangles)
|
||||
|
||||
screen.fill((30, 30, 30))
|
||||
|
||||
# Draw each rectangle (the "objects") in white.
|
||||
for rect in rectangles:
|
||||
pygame.draw.rect(screen, (255, 255, 255), rect, 2)
|
||||
|
||||
# Optionally draw the BVH overlay.
|
||||
if show_bvh and bvh_root:
|
||||
draw_bvh(bvh_root, screen)
|
||||
|
||||
# Debug text instructions.
|
||||
debug_text = font.render("Press D to toggle BVH overlay, R to regenerate rectangles", True, (255, 255, 255))
|
||||
screen.blit(debug_text, (10, 10))
|
||||
|
||||
pygame.display.flip()
|
||||
clock.tick(60)
|
||||
|
||||
pygame.quit()
|
||||
sys.exit()
|
493
bounding-box-thing/main.py
Normal file
493
bounding-box-thing/main.py
Normal file
@ -0,0 +1,493 @@
|
||||
import pygame, sys, random, math
|
||||
|
||||
pygame.init()
|
||||
screen = pygame.display.set_mode((800,600))
|
||||
pygame.display.set_caption("Realistic Rotational Physics Derived from Collisions")
|
||||
clock = pygame.time.Clock()
|
||||
font = pygame.font.SysFont(None, 20)
|
||||
|
||||
# Physics constants
|
||||
GRAVITY = 500.0 # pixels/s^2
|
||||
RESTITUTION = 0.8 # bounciness coefficient
|
||||
DAMPING = 0.99 # linear damping per frame
|
||||
ANGULAR_DAMPING = 0.99 # angular damping per frame
|
||||
|
||||
# Friction coefficient for collisions
|
||||
FRICTION_COEFF = 0.5
|
||||
|
||||
# Modes
|
||||
MODE_EDIT = "edit"
|
||||
MODE_SIMULATE = "simulate"
|
||||
mode = MODE_EDIT
|
||||
|
||||
# Global lists and undo/redo stacks
|
||||
shapes = []
|
||||
undo_stack = []
|
||||
redo_stack = []
|
||||
|
||||
# State for drawing a new shape
|
||||
drawing_new_shape = False
|
||||
current_drawing_points = [] # absolute coordinates
|
||||
|
||||
# State for dragging a shape
|
||||
dragging_shape = None
|
||||
drag_offset = (0,0)
|
||||
drag_start_offset = None
|
||||
|
||||
# Collision mesh grid square size (for visual oriented boxes)
|
||||
square_size = 10
|
||||
|
||||
# Debug flag and counters
|
||||
debug_mode = False
|
||||
collision_checks = 0 # number of SAT axis tests
|
||||
impulse_resolutions_count = 0 # count of impulse resolutions performed
|
||||
|
||||
def rotate_point(x, y, angle):
|
||||
"""Rotate a point (x,y) by angle (in radians)."""
|
||||
cos_a = math.cos(angle)
|
||||
sin_a = math.sin(angle)
|
||||
return (x*cos_a - y*sin_a, x*sin_a + y*cos_a)
|
||||
|
||||
def compute_moment_of_inertia(local_poly, mass):
|
||||
"""Approximate moment of inertia (using maximum distance from the local center)."""
|
||||
cx = sum(p[0] for p in local_poly)/len(local_poly)
|
||||
cy = sum(p[1] for p in local_poly)/len(local_poly)
|
||||
max_dist_sq = max((p[0]-cx)**2 + (p[1]-cy)**2 for p in local_poly)
|
||||
return mass * max_dist_sq / 2
|
||||
|
||||
def point_in_polygon(x, y, poly):
|
||||
"""Ray–casting algorithm to test if (x,y) is inside poly."""
|
||||
num = len(poly)
|
||||
j = num - 1
|
||||
inside = False
|
||||
for i in range(num):
|
||||
xi, yi = poly[i]
|
||||
xj, yj = poly[j]
|
||||
if ((yi > y) != (yj > y)) and (x < (xj-xi)*(y-yi)/(yj-yi+1e-9)+xi):
|
||||
inside = not inside
|
||||
j = i
|
||||
return inside
|
||||
|
||||
def greedy_mesh(grid, rows, cols, square_size):
|
||||
"""Combine adjacent True cells into larger rectangles (in local coordinates)."""
|
||||
used = [[False for _ in range(cols)] for _ in range(rows)]
|
||||
rects = []
|
||||
for row in range(rows):
|
||||
for col in range(cols):
|
||||
if grid[row][col] and not used[row][col]:
|
||||
width = 0
|
||||
while col+width < cols and grid[row][col+width] and not used[row][col+width]:
|
||||
width += 1
|
||||
height = 0
|
||||
valid = True
|
||||
while valid and row+height < rows:
|
||||
for i in range(width):
|
||||
if not grid[row+height][col+i] or used[row+height][col+i]:
|
||||
valid = False
|
||||
break
|
||||
if valid:
|
||||
height += 1
|
||||
for r in range(row, row+height):
|
||||
for c in range(col, col+width):
|
||||
used[r][c] = True
|
||||
rects.append(pygame.Rect(col*square_size, row*square_size, width*square_size, height*square_size))
|
||||
return rects
|
||||
|
||||
def compute_local_collision_boxes(local_poly, square_size):
|
||||
"""Compute grid cells inside local_poly and merge them using greedy meshing."""
|
||||
xs = [p[0] for p in local_poly]
|
||||
ys = [p[1] for p in local_poly]
|
||||
max_x = int(max(xs))
|
||||
max_y = int(max(ys))
|
||||
cols = (max_x // square_size) + 1
|
||||
rows = (max_y // square_size) + 1
|
||||
grid = [[False for _ in range(cols)] for _ in range(rows)]
|
||||
for row in range(rows):
|
||||
for col in range(cols):
|
||||
cx = col*square_size + square_size/2
|
||||
cy = row*square_size + square_size/2
|
||||
if point_in_polygon(cx, cy, local_poly):
|
||||
grid[row][col] = True
|
||||
rects = greedy_mesh(grid, rows, cols, square_size)
|
||||
return rects
|
||||
|
||||
def sat_collision_polygon(polyA, polyB):
|
||||
"""
|
||||
Uses the Separating Axis Theorem (SAT) to check collision between two convex polygons.
|
||||
Returns (colliding, mtv_normal, penetration_depth).
|
||||
Increments the global collision_checks count for each axis test.
|
||||
"""
|
||||
global collision_checks
|
||||
min_overlap = float('inf')
|
||||
mtv_axis = None
|
||||
for polygon in (polyA, polyB):
|
||||
for i in range(len(polygon)):
|
||||
p1 = polygon[i]
|
||||
p2 = polygon[(i+1)%len(polygon)]
|
||||
edge = (p2[0]-p1[0], p2[1]-p1[1])
|
||||
axis = (-edge[1], edge[0])
|
||||
length = math.hypot(axis[0], axis[1])
|
||||
if length == 0:
|
||||
continue
|
||||
axis = (axis[0]/length, axis[1]/length)
|
||||
collision_checks += 1
|
||||
minA, maxA = float('inf'), float('-inf')
|
||||
for p in polyA:
|
||||
proj = p[0]*axis[0] + p[1]*axis[1]
|
||||
minA = min(minA, proj)
|
||||
maxA = max(maxA, proj)
|
||||
minB, maxB = float('inf'), float('-inf')
|
||||
for p in polyB:
|
||||
proj = p[0]*axis[0] + p[1]*axis[1]
|
||||
minB = min(minB, proj)
|
||||
maxB = max(maxB, proj)
|
||||
overlap = min(maxA, maxB) - max(minA, minB)
|
||||
if overlap < 0:
|
||||
return (False, None, 0)
|
||||
if overlap < min_overlap:
|
||||
min_overlap = overlap
|
||||
mtv_axis = axis
|
||||
centerA = (sum(p[0] for p in polyA)/len(polyA), sum(p[1] for p in polyA)/len(polyA))
|
||||
centerB = (sum(p[0] for p in polyB)/len(polyB), sum(p[1] for p in polyB)/len(polyB))
|
||||
d = (centerB[0]-centerA[0], centerB[1]-centerA[1])
|
||||
if d[0]*mtv_axis[0] + d[1]*mtv_axis[1] < 0:
|
||||
mtv_axis = (-mtv_axis[0], -mtv_axis[1])
|
||||
return (True, mtv_axis, min_overlap)
|
||||
|
||||
class Shape:
|
||||
def __init__(self, local_poly, offset):
|
||||
"""
|
||||
local_poly: list of points (x,y) in local coordinates (with minimum at (0,0)).
|
||||
offset: (x,y) position in the window.
|
||||
"""
|
||||
self.local_poly = local_poly
|
||||
self.offset = list(offset)
|
||||
self.collision_rects = compute_local_collision_boxes(self.local_poly, square_size)
|
||||
self.velocity = [0.0, 0.0]
|
||||
self.angular_velocity = 0.0 # starts at zero; rotation will be derived from collisions
|
||||
self.angle = 0.0
|
||||
self.color = (random.randint(50,255), random.randint(50,255), random.randint(50,255))
|
||||
area = abs(0.5 * sum(local_poly[i][0]*local_poly[(i+1)%len(local_poly)][1] - local_poly[(i+1)%len(local_poly)][0]*local_poly[i][1]
|
||||
for i in range(len(local_poly))))
|
||||
self.mass = area if area > 0 else 1.0
|
||||
self.inertia = compute_moment_of_inertia(local_poly, self.mass)
|
||||
self.local_center = (sum(p[0] for p in local_poly)/len(local_poly),
|
||||
sum(p[1] for p in local_poly)/len(local_poly))
|
||||
self.colliding_boxes = set()
|
||||
|
||||
def get_absolute_polygon(self):
|
||||
"""Return the rotated and translated polygon."""
|
||||
abs_poly = []
|
||||
for p in self.local_poly:
|
||||
rel = (p[0]-self.local_center[0], p[1]-self.local_center[1])
|
||||
rot = rotate_point(rel[0], rel[1], self.angle)
|
||||
abs_poly.append((rot[0] + self.offset[0] + self.local_center[0],
|
||||
rot[1] + self.offset[1] + self.local_center[1]))
|
||||
return abs_poly
|
||||
|
||||
def get_absolute_collision_polys(self):
|
||||
"""Return oriented collision boxes (for visualization)."""
|
||||
polys = []
|
||||
for rect in self.collision_rects:
|
||||
corners = [(rect.x, rect.y),
|
||||
(rect.x+rect.width, rect.y),
|
||||
(rect.x+rect.width, rect.y+rect.height),
|
||||
(rect.x, rect.y+rect.height)]
|
||||
transformed = []
|
||||
for pt in corners:
|
||||
rel = (pt[0]-self.local_center[0], pt[1]-self.local_center[1])
|
||||
rot = rotate_point(rel[0], rel[1], self.angle)
|
||||
transformed.append((rot[0] + self.offset[0] + self.local_center[0],
|
||||
rot[1] + self.offset[1] + self.local_center[1]))
|
||||
polys.append(transformed)
|
||||
return polys
|
||||
|
||||
def get_bounding_rect(self):
|
||||
poly = self.get_absolute_polygon()
|
||||
xs = [p[0] for p in poly]
|
||||
ys = [p[1] for p in poly]
|
||||
return pygame.Rect(min(xs), min(ys), max(xs)-min(xs), max(ys)-min(ys))
|
||||
|
||||
def get_center(self):
|
||||
"""Return the global center (local center translated)."""
|
||||
return (self.offset[0] + self.local_center[0],
|
||||
self.offset[1] + self.local_center[1])
|
||||
|
||||
def update(self, dt):
|
||||
# Apply gravity.
|
||||
self.velocity[1] += GRAVITY * dt
|
||||
# Update position.
|
||||
self.offset[0] += self.velocity[0] * dt
|
||||
self.offset[1] += self.velocity[1] * dt
|
||||
# Update rotation.
|
||||
self.angle += self.angular_velocity * dt
|
||||
# Bounce off window edges.
|
||||
br = self.get_bounding_rect()
|
||||
if br.left < 0:
|
||||
self.offset[0] -= br.left
|
||||
self.velocity[0] = -RESTITUTION * self.velocity[0]
|
||||
if br.right > screen.get_width():
|
||||
self.offset[0] -= (br.right - screen.get_width())
|
||||
self.velocity[0] = -RESTITUTION * self.velocity[0]
|
||||
if br.top < 0:
|
||||
self.offset[1] -= br.top
|
||||
self.velocity[1] = -RESTITUTION * self.velocity[1]
|
||||
if br.bottom > screen.get_height():
|
||||
self.offset[1] -= (br.bottom - screen.get_height())
|
||||
self.velocity[1] = -RESTITUTION * self.velocity[1]
|
||||
self.velocity[0] *= DAMPING
|
||||
self.velocity[1] *= DAMPING
|
||||
self.angular_velocity *= ANGULAR_DAMPING
|
||||
|
||||
def draw(self, surface):
|
||||
pygame.draw.polygon(surface, self.color, self.get_absolute_polygon(), 2)
|
||||
polys = self.get_absolute_collision_polys()
|
||||
for idx, poly in enumerate(polys):
|
||||
col = (255,0,0) if idx in self.colliding_boxes else (0,0,255)
|
||||
pygame.draw.polygon(surface, col, poly, 1)
|
||||
|
||||
def resolve_collision_realistic(shapeA, shapeB, normal, penetration):
|
||||
"""
|
||||
Applies positional correction and impulse–based collision resolution (with friction)
|
||||
so that rotation is derived from collision impulses.
|
||||
"""
|
||||
global impulse_resolutions_count
|
||||
percent = 0.2
|
||||
slop = 0.01
|
||||
inv_massA = 1/shapeA.mass
|
||||
inv_massB = 1/shapeB.mass
|
||||
correction = (max(penetration - slop, 0) / (inv_massA + inv_massB)) * percent
|
||||
shapeA.offset[0] -= correction * inv_massA * normal[0]
|
||||
shapeA.offset[1] -= correction * inv_massA * normal[1]
|
||||
shapeB.offset[0] += correction * inv_massB * normal[0]
|
||||
shapeB.offset[1] += correction * inv_massB * normal[1]
|
||||
|
||||
centerA = shapeA.get_center()
|
||||
centerB = shapeB.get_center()
|
||||
contact_point = ((centerA[0]+centerB[0])/2, (centerA[1]+centerB[1])/2)
|
||||
rA = (contact_point[0]-centerA[0], contact_point[1]-centerA[1])
|
||||
rB = (contact_point[0]-centerB[0], contact_point[1]-centerB[1])
|
||||
velA_contact = (shapeA.velocity[0] + -shapeA.angular_velocity * rA[1],
|
||||
shapeA.velocity[1] + shapeA.angular_velocity * rA[0])
|
||||
velB_contact = (shapeB.velocity[0] + -shapeB.angular_velocity * rB[1],
|
||||
shapeB.velocity[1] + shapeB.angular_velocity * rB[0])
|
||||
rv = (velB_contact[0]-velA_contact[0], velB_contact[1]-velA_contact[1])
|
||||
vel_along_normal = rv[0]*normal[0] + rv[1]*normal[1]
|
||||
if vel_along_normal > 0:
|
||||
return
|
||||
e = RESTITUTION
|
||||
rA_cross_n = rA[0]*normal[1] - rA[1]*normal[0]
|
||||
rB_cross_n = rB[0]*normal[1] - rB[1]*normal[0]
|
||||
inv_inertiaA = 1/shapeA.inertia if shapeA.inertia != 0 else 0
|
||||
inv_inertiaB = 1/shapeB.inertia if shapeB.inertia != 0 else 0
|
||||
denom = inv_massA + inv_massB + (rA_cross_n**2)*inv_inertiaA + (rB_cross_n**2)*inv_inertiaB
|
||||
if denom == 0:
|
||||
return
|
||||
j = -(1+e)*vel_along_normal/denom
|
||||
impulse = (j*normal[0], j*normal[1])
|
||||
shapeA.velocity[0] -= impulse[0]*inv_massA
|
||||
shapeA.velocity[1] -= impulse[1]*inv_massA
|
||||
shapeB.velocity[0] += impulse[0]*inv_massB
|
||||
shapeB.velocity[1] += impulse[1]*inv_massB
|
||||
shapeA.angular_velocity -= rA_cross_n * j * inv_inertiaA
|
||||
shapeB.angular_velocity += rB_cross_n * j * inv_inertiaB
|
||||
|
||||
tangent = (rv[0]-vel_along_normal*normal[0],
|
||||
rv[1]-vel_along_normal*normal[1])
|
||||
t_length = math.hypot(tangent[0], tangent[1])
|
||||
if t_length != 0:
|
||||
tangent = (tangent[0]/t_length, tangent[1]/t_length)
|
||||
jt = - (rv[0]*tangent[0] + rv[1]*tangent[1]) / denom
|
||||
jt = max(-j*FRICTION_COEFF, min(jt, j*FRICTION_COEFF))
|
||||
friction_impulse = (jt*tangent[0], jt*tangent[1])
|
||||
shapeA.velocity[0] -= friction_impulse[0]*inv_massA
|
||||
shapeA.velocity[1] -= friction_impulse[1]*inv_massA
|
||||
shapeB.velocity[0] += friction_impulse[0]*inv_massB
|
||||
shapeB.velocity[1] += friction_impulse[1]*inv_massB
|
||||
shapeA.angular_velocity -= (rA[0]*tangent[1] - rA[1]*tangent[0]) * jt * inv_inertiaA
|
||||
shapeB.angular_velocity += (rB[0]*tangent[1] - rB[1]*tangent[0]) * jt * inv_inertiaB
|
||||
global impulse_resolutions_count
|
||||
impulse_resolutions_count += 1
|
||||
|
||||
class Button:
|
||||
def __init__(self, rect, text):
|
||||
self.rect = pygame.Rect(rect)
|
||||
self.text = text
|
||||
def draw(self, surface):
|
||||
pygame.draw.rect(surface, (100,100,100), self.rect)
|
||||
txt = font.render(self.text, True, (255,255,255))
|
||||
txt_rect = txt.get_rect(center=self.rect.center)
|
||||
surface.blit(txt, txt_rect)
|
||||
def is_clicked(self, pos):
|
||||
return self.rect.collidepoint(pos)
|
||||
|
||||
new_shape_btn = Button((10,10,100,30), "New Shape")
|
||||
play_btn = Button((120,10,100,30), "Play")
|
||||
undo_btn = Button((230,10,100,30), "Undo")
|
||||
redo_btn = Button((340,10,100,30), "Redo")
|
||||
edit_btn = Button((450,10,100,30), "Edit")
|
||||
|
||||
running = True
|
||||
while running:
|
||||
dt = clock.tick(60)/1000.0
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_d:
|
||||
debug_mode = not debug_mode
|
||||
if mode == MODE_EDIT:
|
||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||
pos = pygame.mouse.get_pos()
|
||||
if new_shape_btn.is_clicked(pos):
|
||||
drawing_new_shape = True
|
||||
current_drawing_points = []
|
||||
dragging_shape = None
|
||||
elif play_btn.is_clicked(pos):
|
||||
mode = MODE_SIMULATE
|
||||
# Launch shapes with random linear velocities but zero angular velocity.
|
||||
for shape in shapes:
|
||||
shape.velocity = [random.choice([-200, -150, 150, 200]),
|
||||
random.choice([-200, -150, 150, 200])]
|
||||
shape.angular_velocity = 0.0
|
||||
elif undo_btn.is_clicked(pos):
|
||||
if undo_stack:
|
||||
action = undo_stack.pop()
|
||||
if action[0] == "add":
|
||||
shape = action[1]
|
||||
if shape in shapes:
|
||||
shapes.remove(shape)
|
||||
redo_stack.append(("add", shape))
|
||||
elif action[0] == "move":
|
||||
shape, old_offset, new_offset = action[1], action[2], action[3]
|
||||
shape.offset = list(old_offset)
|
||||
redo_stack.append(("move", shape, new_offset, old_offset))
|
||||
elif redo_btn.is_clicked(pos):
|
||||
if redo_stack:
|
||||
action = redo_stack.pop()
|
||||
if action[0] == "add":
|
||||
shape = action[1]
|
||||
shapes.append(shape)
|
||||
undo_stack.append(("add", shape))
|
||||
elif action[0] == "move":
|
||||
shape, old_offset, new_offset = action[1], action[2], action[3]
|
||||
shape.offset = list(new_offset)
|
||||
undo_stack.append(("move", shape, old_offset, new_offset))
|
||||
elif edit_btn.is_clicked(pos):
|
||||
pass
|
||||
else:
|
||||
if drawing_new_shape:
|
||||
current_drawing_points.append(pos)
|
||||
else:
|
||||
for shape in reversed(shapes):
|
||||
if point_in_polygon(pos[0], pos[1], shape.get_absolute_polygon()):
|
||||
dragging_shape = shape
|
||||
drag_offset = (pos[0]-shape.offset[0], pos[1]-shape.offset[1])
|
||||
drag_start_offset = shape.offset.copy()
|
||||
break
|
||||
elif event.type == pygame.MOUSEBUTTONUP:
|
||||
if dragging_shape:
|
||||
if drag_start_offset != dragging_shape.offset:
|
||||
undo_stack.append(("move", dragging_shape, drag_start_offset, dragging_shape.offset.copy()))
|
||||
redo_stack.clear()
|
||||
dragging_shape = None
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_RETURN and drawing_new_shape and len(current_drawing_points) >= 3:
|
||||
xs = [p[0] for p in current_drawing_points]
|
||||
ys = [p[1] for p in current_drawing_points]
|
||||
min_x, min_y = min(xs), min(ys)
|
||||
local_poly = [(p[0]-min_x, p[1]-min_y) for p in current_drawing_points]
|
||||
new_shape = Shape(local_poly, (min_x, min_y))
|
||||
shapes.append(new_shape)
|
||||
undo_stack.append(("add", new_shape))
|
||||
redo_stack.clear()
|
||||
drawing_new_shape = False
|
||||
elif event.key == pygame.K_z:
|
||||
if undo_stack:
|
||||
action = undo_stack.pop()
|
||||
if action[0] == "add":
|
||||
shape = action[1]
|
||||
if shape in shapes:
|
||||
shapes.remove(shape)
|
||||
redo_stack.append(("add", shape))
|
||||
elif action[0] == "move":
|
||||
shape, old_offset, new_offset = action[1], action[2], action[3]
|
||||
shape.offset = list(old_offset)
|
||||
redo_stack.append(("move", shape, new_offset, old_offset))
|
||||
elif event.key == pygame.K_y:
|
||||
if redo_stack:
|
||||
action = redo_stack.pop()
|
||||
if action[0] == "add":
|
||||
shape = action[1]
|
||||
shapes.append(shape)
|
||||
undo_stack.append(("add", shape))
|
||||
elif action[0] == "move":
|
||||
shape, old_offset, new_offset = action[1], action[2], action[3]
|
||||
shape.offset = list(new_offset)
|
||||
undo_stack.append(("move", shape, old_offset, new_offset))
|
||||
elif mode == MODE_SIMULATE:
|
||||
if event.type == pygame.MOUSEBUTTONDOWN:
|
||||
pos = pygame.mouse.get_pos()
|
||||
if edit_btn.is_clicked(pos):
|
||||
mode = MODE_EDIT
|
||||
for shape in shapes:
|
||||
shape.velocity = [0.0, 0.0]
|
||||
shape.angular_velocity = 0.0
|
||||
if event.type == pygame.KEYDOWN:
|
||||
if event.key == pygame.K_ESCAPE:
|
||||
running = False
|
||||
if mode == MODE_EDIT and dragging_shape:
|
||||
mpos = pygame.mouse.get_pos()
|
||||
dragging_shape.offset[0] = mpos[0]-drag_offset[0]
|
||||
dragging_shape.offset[1] = mpos[1]-drag_offset[1]
|
||||
if mode == MODE_SIMULATE:
|
||||
for shape in shapes:
|
||||
shape.update(dt)
|
||||
shape.colliding_boxes = set()
|
||||
collision_checks = 0
|
||||
impulse_resolutions_count = 0
|
||||
for i in range(len(shapes)):
|
||||
for j in range(i+1, len(shapes)):
|
||||
shapeA = shapes[i]
|
||||
shapeB = shapes[j]
|
||||
polyA = shapeA.get_absolute_polygon()
|
||||
polyB = shapeB.get_absolute_polygon()
|
||||
colliding, normal, penetration = sat_collision_polygon(polyA, polyB)
|
||||
if colliding:
|
||||
resolve_collision_realistic(shapeA, shapeB, normal, penetration)
|
||||
screen.fill((30,30,30))
|
||||
new_shape_btn.draw(screen)
|
||||
play_btn.draw(screen)
|
||||
undo_btn.draw(screen)
|
||||
redo_btn.draw(screen)
|
||||
edit_btn.draw(screen)
|
||||
if drawing_new_shape:
|
||||
if len(current_drawing_points) > 1:
|
||||
pygame.draw.lines(screen, (255,255,255), False, current_drawing_points, 2)
|
||||
for p in current_drawing_points:
|
||||
pygame.draw.circle(screen, (255,0,0), p, 4)
|
||||
for shape in shapes:
|
||||
shape.draw(screen)
|
||||
if mode == MODE_SIMULATE:
|
||||
sim_text = font.render("Simulation Mode - Click Edit to return", True, (255,255,255))
|
||||
screen.blit(sim_text, (10,50))
|
||||
else:
|
||||
edit_text = font.render("Edit Mode - Draw/Drag shapes. Enter to finish shape.", True, (255,255,255))
|
||||
screen.blit(edit_text, (10,50))
|
||||
if debug_mode:
|
||||
debug_lines = []
|
||||
debug_lines.append(f"Mode: {mode}")
|
||||
debug_lines.append(f"Shapes: {len(shapes)}")
|
||||
total_boxes = sum(len(s.get_absolute_collision_polys()) for s in shapes)
|
||||
debug_lines.append(f"Oriented Collision Boxes: {total_boxes}")
|
||||
debug_lines.append(f"SAT Axis Tests: {collision_checks}")
|
||||
debug_lines.append(f"Impulse Resolutions: {impulse_resolutions_count}")
|
||||
debug_lines.append(f"FPS: {int(clock.get_fps())}")
|
||||
for i, shape in enumerate(shapes):
|
||||
debug_lines.append(f"Shape {i}: angle={math.degrees(shape.angle):.1f}°, ang_vel={shape.angular_velocity:.2f}")
|
||||
for idx, line in enumerate(debug_lines):
|
||||
txt = font.render(line, True, (255,255,0))
|
||||
screen.blit(txt, (10, 90+idx*18))
|
||||
pygame.display.flip()
|
||||
pygame.quit()
|
||||
sys.exit()
|
83
bounding-box-thing/test2.py
Normal file
83
bounding-box-thing/test2.py
Normal file
@ -0,0 +1,83 @@
|
||||
import pygame
|
||||
import sys
|
||||
|
||||
# Initialize Pygame and create window
|
||||
pygame.init()
|
||||
screen = pygame.display.set_mode((800, 600))
|
||||
pygame.display.set_caption("Freehand Drawing with Debug Info")
|
||||
font = pygame.font.Font(None, 24)
|
||||
clock = pygame.time.Clock()
|
||||
|
||||
# Data structures
|
||||
shapes = [] # List of completed shapes; each is a dict with points and bbox
|
||||
drawing = False
|
||||
current_points = [] # List of points for the stroke being drawn
|
||||
|
||||
def compute_bounding_box(points):
|
||||
if not points:
|
||||
return None
|
||||
xs = [p[0] for p in points]
|
||||
ys = [p[1] for p in points]
|
||||
min_x, max_x = min(xs), max(xs)
|
||||
min_y, max_y = min(ys), max(ys)
|
||||
return (min_x, min_y, max_x - min_x, max_y - min_y)
|
||||
|
||||
while True:
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
pygame.quit()
|
||||
sys.exit()
|
||||
# Begin drawing on mouse button down
|
||||
elif event.type == pygame.MOUSEBUTTONDOWN and event.button == 1:
|
||||
drawing = True
|
||||
current_points = [event.pos]
|
||||
# Append points as the mouse moves while drawing
|
||||
elif event.type == pygame.MOUSEMOTION and drawing:
|
||||
current_points.append(event.pos)
|
||||
# Finish the current stroke on mouse button release
|
||||
elif event.type == pygame.MOUSEBUTTONUP and event.button == 1:
|
||||
drawing = False
|
||||
bbox = compute_bounding_box(current_points)
|
||||
shapes.append({
|
||||
"points": current_points,
|
||||
"bbox": bbox
|
||||
})
|
||||
current_points = []
|
||||
|
||||
# Clear screen
|
||||
screen.fill((30, 30, 30))
|
||||
|
||||
# Draw completed shapes in white
|
||||
for shape in shapes:
|
||||
if len(shape["points"]) > 1:
|
||||
pygame.draw.lines(screen, (255, 255, 255), False, shape["points"], 2)
|
||||
if shape["bbox"]:
|
||||
pygame.draw.rect(screen, (255, 255, 255), shape["bbox"], 1)
|
||||
|
||||
# Draw current shape (freehand drawing) in green
|
||||
if drawing and current_points:
|
||||
if len(current_points) > 1:
|
||||
pygame.draw.lines(screen, (0, 255, 0), False, current_points, 2)
|
||||
bbox = compute_bounding_box(current_points)
|
||||
if bbox:
|
||||
pygame.draw.rect(screen, (0, 255, 0), bbox, 1)
|
||||
|
||||
# Debug Menu Info
|
||||
debug_lines = [
|
||||
f"Drawing: {drawing}",
|
||||
f"Current Points: {len(current_points)}",
|
||||
f"Shapes drawn: {len(shapes)}"
|
||||
]
|
||||
if current_points:
|
||||
bbox = compute_bounding_box(current_points)
|
||||
debug_lines.append(f"Bounding Box: {bbox}")
|
||||
|
||||
# Render debug text
|
||||
y_offset = 10
|
||||
for line in debug_lines:
|
||||
text_surface = font.render(line, True, (255, 255, 255))
|
||||
screen.blit(text_surface, (10, y_offset))
|
||||
y_offset += text_surface.get_height() + 5
|
||||
|
||||
pygame.display.flip()
|
||||
clock.tick(60)
|
299
lighting/ray-lighting.py
Normal file
299
lighting/ray-lighting.py
Normal file
@ -0,0 +1,299 @@
|
||||
import pygame
|
||||
import sys
|
||||
import math
|
||||
import random
|
||||
|
||||
# -----------------------------
|
||||
# Pygame and world initialization
|
||||
# -----------------------------
|
||||
pygame.init()
|
||||
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
|
||||
BLOCK_SIZE = 40
|
||||
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
|
||||
pygame.display.set_caption("2D Voxel Game with Advanced Debug Visualization")
|
||||
clock = pygame.time.Clock()
|
||||
font = pygame.font.Font(None, 20)
|
||||
|
||||
# -----------------------------
|
||||
# Global debug options
|
||||
# -----------------------------
|
||||
debug_mode = False # Toggle full debug overlay (F3)
|
||||
show_bvh = False # Toggle drawing BVH overlay (D)
|
||||
num_rays = 360 * 2 # Rays cast for lighting
|
||||
max_distance = 1000 # Maximum ray distance if nothing is hit
|
||||
|
||||
# Global counter for ray intersection tests
|
||||
ray_intersect_count = 0
|
||||
|
||||
# -----------------------------
|
||||
# Create a simple voxel world
|
||||
# -----------------------------
|
||||
# Build a grid of blocks (some cells are solid, some are empty)
|
||||
def generate_blocks():
|
||||
blocks = []
|
||||
cols = SCREEN_WIDTH // BLOCK_SIZE
|
||||
rows = SCREEN_HEIGHT // BLOCK_SIZE
|
||||
for i in range(cols):
|
||||
for j in range(rows):
|
||||
# For demo purposes, randomly assign some blocks as solid (20% chance)
|
||||
if random.random() < 0.2:
|
||||
blocks.append(pygame.Rect(i * BLOCK_SIZE, j * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE))
|
||||
return blocks
|
||||
|
||||
blocks = generate_blocks()
|
||||
|
||||
# -----------------------------
|
||||
# BVH Data Structures and Build
|
||||
# -----------------------------
|
||||
# BVH Node: holds a bounding box (min_x, min_y, max_x, max_y) and either children or a solid block.
|
||||
class BVHNode:
|
||||
def __init__(self, bbox, left=None, right=None, block=None):
|
||||
self.bbox = bbox # Tuple: (min_x, min_y, max_x, max_y)
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.block = block # For leaf nodes, store the actual block (pygame.Rect)
|
||||
|
||||
def rect_to_bbox(rect):
|
||||
return (rect.left, rect.top, rect.right, rect.bottom)
|
||||
|
||||
def union_bbox(b1, b2):
|
||||
x1 = min(b1[0], b2[0])
|
||||
y1 = min(b1[1], b2[1])
|
||||
x2 = max(b1[2], b2[2])
|
||||
y2 = max(b1[3], b2[3])
|
||||
return (x1, y1, x2, y2)
|
||||
|
||||
def build_bvh(block_list):
|
||||
if not block_list:
|
||||
return None
|
||||
if len(block_list) == 1:
|
||||
return BVHNode(rect_to_bbox(block_list[0]), block=block_list[0])
|
||||
# Sort blocks by center x-coordinate (could also choose y or alternate)
|
||||
block_list.sort(key=lambda rect: rect.centerx)
|
||||
mid = len(block_list) // 2
|
||||
left = build_bvh(block_list[:mid])
|
||||
right = build_bvh(block_list[mid:])
|
||||
if left and right:
|
||||
bbox = union_bbox(left.bbox, right.bbox)
|
||||
elif left:
|
||||
bbox = left.bbox
|
||||
else:
|
||||
bbox = right.bbox
|
||||
return BVHNode(bbox, left, right)
|
||||
|
||||
bvh_root = build_bvh(blocks)
|
||||
|
||||
# -----------------------------
|
||||
# BVH Statistics Functions
|
||||
# -----------------------------
|
||||
def get_bvh_stats(node):
|
||||
"""Return (node_count, depth) for the given BVH."""
|
||||
if node is None:
|
||||
return (0, 0)
|
||||
if node.block is not None:
|
||||
return (1, 1)
|
||||
left_count, left_depth = get_bvh_stats(node.left)
|
||||
right_count, right_depth = get_bvh_stats(node.right)
|
||||
total = 1 + left_count + right_count
|
||||
depth = 1 + max(left_depth, right_depth)
|
||||
return (total, depth)
|
||||
|
||||
# -----------------------------
|
||||
# Ray-AABB Intersection (Slab Method)
|
||||
# -----------------------------
|
||||
def ray_intersect_aabb(origin, direction, bbox):
|
||||
# Increase global counter each time this function is called.
|
||||
global ray_intersect_count
|
||||
ray_intersect_count += 1
|
||||
|
||||
# bbox: (min_x, min_y, max_x, max_y)
|
||||
tmin = -math.inf
|
||||
tmax = math.inf
|
||||
ox, oy = origin
|
||||
dx, dy = direction
|
||||
|
||||
# X slab
|
||||
if dx != 0:
|
||||
tx1 = (bbox[0] - ox) / dx
|
||||
tx2 = (bbox[2] - ox) / dx
|
||||
tmin = max(tmin, min(tx1, tx2))
|
||||
tmax = min(tmax, max(tx1, tx2))
|
||||
else:
|
||||
if not (bbox[0] <= ox <= bbox[2]):
|
||||
return None
|
||||
|
||||
# Y slab
|
||||
if dy != 0:
|
||||
ty1 = (bbox[1] - oy) / dy
|
||||
ty2 = (bbox[3] - oy) / dy
|
||||
tmin = max(tmin, min(ty1, ty2))
|
||||
tmax = min(tmax, max(ty1, ty2))
|
||||
else:
|
||||
if not (bbox[1] <= oy <= bbox[3]):
|
||||
return None
|
||||
|
||||
if tmax >= tmin and tmax >= 0:
|
||||
return tmin if tmin >= 0 else tmax
|
||||
return None
|
||||
|
||||
# -----------------------------
|
||||
# BVH Ray Casting
|
||||
# -----------------------------
|
||||
def ray_cast_bvh(node, origin, direction):
|
||||
if node is None:
|
||||
return None, None
|
||||
# Test if ray intersects node's bounding box.
|
||||
t_bbox = ray_intersect_aabb(origin, direction, node.bbox)
|
||||
if t_bbox is None:
|
||||
return None, None
|
||||
|
||||
# If leaf node, test collision with the block.
|
||||
if node.block is not None:
|
||||
t_hit = ray_intersect_aabb(origin, direction, rect_to_bbox(node.block))
|
||||
if t_hit is not None:
|
||||
return t_hit, node.block
|
||||
else:
|
||||
return None, None
|
||||
|
||||
# Otherwise, recursively test both children.
|
||||
t_left, block_left = ray_cast_bvh(node.left, origin, direction)
|
||||
t_right, block_right = ray_cast_bvh(node.right, origin, direction)
|
||||
|
||||
if t_left is not None and t_right is not None:
|
||||
if t_left < t_right:
|
||||
return t_left, block_left
|
||||
else:
|
||||
return t_right, block_right
|
||||
elif t_left is not None:
|
||||
return t_left, block_left
|
||||
elif t_right is not None:
|
||||
return t_right, block_right
|
||||
return None, None
|
||||
|
||||
# -----------------------------
|
||||
# Ray-Based Lighting Function
|
||||
# -----------------------------
|
||||
# Cast many rays from the light source (controlled by the mouse) and collect hit points.
|
||||
light_source = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
|
||||
|
||||
def cast_light_rays(light_pos):
|
||||
points = []
|
||||
# Cast one ray per degree
|
||||
for angle in range(0, 360, num_rays // num_rays):
|
||||
rad = math.radians(angle)
|
||||
direction = (math.cos(rad), math.sin(rad))
|
||||
t, _ = ray_cast_bvh(bvh_root, light_pos, direction)
|
||||
if t is None:
|
||||
t = max_distance
|
||||
hit_x = light_pos[0] + direction[0] * t
|
||||
hit_y = light_pos[1] + direction[1] * t
|
||||
points.append((hit_x, hit_y))
|
||||
return points
|
||||
|
||||
# -----------------------------
|
||||
# Advanced Debug Menu Drawing
|
||||
# -----------------------------
|
||||
def draw_debug_menu(surface, fps):
|
||||
# Gather BVH statistics.
|
||||
bvh_node_count, bvh_depth = get_bvh_stats(bvh_root)
|
||||
# Prepare debug information lines.
|
||||
debug_lines = [
|
||||
"DEBUG MODE ACTIVE",
|
||||
f"FPS: {fps:.1f}",
|
||||
f"Blocks: {len(blocks)}",
|
||||
f"BVH Nodes: {bvh_node_count}",
|
||||
f"BVH Depth: {bvh_depth}",
|
||||
f"Light Source: {light_source}",
|
||||
f"Rays Cast: {num_rays}",
|
||||
f"Ray Intersection Tests (this frame): {ray_intersect_count}",
|
||||
f"Avg Tests per Ray: {ray_intersect_count / num_rays:.2f}",
|
||||
"Toggles: F3 - Debug, D - BVH overlay, R - Regenerate World"
|
||||
]
|
||||
# Create a semi-transparent panel background.
|
||||
panel_width = 300
|
||||
panel_height = (len(debug_lines) * 22) + 10
|
||||
panel = pygame.Surface((panel_width, panel_height))
|
||||
panel.set_alpha(180)
|
||||
panel.fill((0, 0, 0))
|
||||
# Render text lines onto the panel.
|
||||
y_offset = 5
|
||||
for line in debug_lines:
|
||||
text_surf = font.render(line, True, (255, 255, 255))
|
||||
panel.blit(text_surf, (5, y_offset))
|
||||
y_offset += 22
|
||||
# Blit the debug panel onto the main surface.
|
||||
surface.blit(panel, (10, 10))
|
||||
|
||||
# -----------------------------
|
||||
# BVH Visualization (recursive drawing)
|
||||
# -----------------------------
|
||||
def draw_bvh(node, surface):
|
||||
if node is None:
|
||||
return
|
||||
x, y, x2, y2 = node.bbox
|
||||
rect = pygame.Rect(x, y, x2 - x, y2 - y)
|
||||
# Draw leaf nodes in green; internal nodes in red.
|
||||
color = (0, 255, 0) if node.block is not None else (255, 0, 0)
|
||||
pygame.draw.rect(surface, color, rect, 1)
|
||||
draw_bvh(node.left, surface)
|
||||
draw_bvh(node.right, surface)
|
||||
|
||||
# -----------------------------
|
||||
# Main Game Loop
|
||||
# -----------------------------
|
||||
running = True
|
||||
while running:
|
||||
# Reset ray intersection counter each frame.
|
||||
ray_intersect_count = 0
|
||||
|
||||
for event in pygame.event.get():
|
||||
if event.type == pygame.QUIT:
|
||||
running = False
|
||||
elif event.type == pygame.KEYDOWN:
|
||||
# Toggle full debug mode with F3.
|
||||
if event.key == pygame.K_F3:
|
||||
debug_mode = not debug_mode
|
||||
# Toggle BVH overlay with D.
|
||||
elif event.key == pygame.K_d:
|
||||
show_bvh = not show_bvh
|
||||
# Regenerate the world and rebuild the BVH with R.
|
||||
elif event.key == pygame.K_r:
|
||||
blocks = generate_blocks()
|
||||
bvh_root = build_bvh(blocks)
|
||||
# Update light source with mouse motion.
|
||||
elif event.type == pygame.MOUSEMOTION:
|
||||
light_source = event.pos
|
||||
|
||||
# Clear screen.
|
||||
screen.fill((30, 30, 30))
|
||||
|
||||
# Draw voxel blocks (solid cells).
|
||||
for block in blocks:
|
||||
pygame.draw.rect(screen, (100, 100, 100), block)
|
||||
|
||||
# Optionally draw the BVH overlay.
|
||||
if show_bvh and bvh_root:
|
||||
draw_bvh(bvh_root, screen)
|
||||
|
||||
# Cast light rays from the light source.
|
||||
light_polygon = cast_light_rays(light_source)
|
||||
# Create a separate surface for lighting effects.
|
||||
light_surface = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
|
||||
light_surface.fill((0, 0, 0))
|
||||
pygame.draw.polygon(light_surface, (255, 255, 200), light_polygon)
|
||||
light_surface.set_alpha(180)
|
||||
screen.blit(light_surface, (0, 0), special_flags=pygame.BLEND_ADD)
|
||||
|
||||
# Draw the light source.
|
||||
pygame.draw.circle(screen, (255, 255, 0), light_source, 5)
|
||||
|
||||
# Draw advanced debug overlay if debug mode is enabled.
|
||||
if debug_mode:
|
||||
fps = clock.get_fps()
|
||||
draw_debug_menu(screen, fps)
|
||||
|
||||
pygame.display.flip()
|
||||
clock.tick(60)
|
||||
|
||||
pygame.quit()
|
||||
sys.exit()
|
252
lighting/ray-lighting2.py
Normal file
252
lighting/ray-lighting2.py
Normal file
@ -0,0 +1,252 @@
|
||||
import pygame
|
||||
import sys
|
||||
import math
|
||||
import random
|
||||
|
||||
# -----------------------------
|
||||
# Pygame and world initialization
|
||||
# -----------------------------
|
||||
pygame.init()
|
||||
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
|
||||
BLOCK_SIZE = 40
|
||||
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
|
||||
pygame.display.set_caption("2D Voxel Game with Collision Pixel Lighting (No Bouncing)")
|
||||
clock = pygame.time.Clock()
|
||||
font = pygame.font.Font(None, 20)
|
||||
|
||||
# -----------------------------
|
||||
# Global Options
|
||||
# -----------------------------
|
||||
debug_mode = False # Toggle full debug overlay (F3)
|
||||
num_rays = 360 * 2 # Number of primary rays to cast
|
||||
max_distance = 1000 # Maximum distance for a ray if nothing is hit
|
||||
# Global counter for ray intersection tests (resets every frame)
|
||||
ray_intersect_count = 0
|
||||
|
||||
# -----------------------------
|
||||
# Voxel World Generation (each block gets a random color)
|
||||
# -----------------------------
|
||||
def generate_blocks():
|
||||
blocks = []
|
||||
cols = SCREEN_WIDTH // BLOCK_SIZE
|
||||
rows = SCREEN_HEIGHT // BLOCK_SIZE
|
||||
for i in range(cols):
|
||||
for j in range(rows):
|
||||
# 20% chance that this grid cell is a solid block.
|
||||
if random.random() < 0.2:
|
||||
rect = pygame.Rect(i * BLOCK_SIZE, j * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
|
||||
color = (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255))
|
||||
# Store block as tuple: (pygame.Rect, color)
|
||||
blocks.append((rect, color))
|
||||
return blocks
|
||||
|
||||
blocks = generate_blocks()
|
||||
|
||||
# -----------------------------
|
||||
# BVH Data Structures and Build
|
||||
# -----------------------------
|
||||
class BVHNode:
|
||||
def __init__(self, bbox, left=None, right=None, block=None):
|
||||
self.bbox = bbox # (min_x, min_y, max_x, max_y)
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.block = block # For leaf nodes, store the block (tuple: (pygame.Rect, color))
|
||||
|
||||
def rect_to_bbox(block):
|
||||
rect = block[0] if isinstance(block, tuple) else block
|
||||
return (rect.left, rect.top, rect.right, rect.bottom)
|
||||
|
||||
def union_bbox(b1, b2):
|
||||
x1 = min(b1[0], b2[0])
|
||||
y1 = min(b1[1], b2[1])
|
||||
x2 = max(b1[2], b2[2])
|
||||
y2 = max(b1[3], b2[3])
|
||||
return (x1, y1, x2, y2)
|
||||
|
||||
def build_bvh(block_list):
|
||||
if not block_list:
|
||||
return None
|
||||
if len(block_list) == 1:
|
||||
return BVHNode(rect_to_bbox(block_list[0]), block=block_list[0])
|
||||
# Sort blocks by center x-coordinate
|
||||
block_list.sort(key=lambda block: block[0].centerx)
|
||||
mid = len(block_list) // 2
|
||||
left = build_bvh(block_list[:mid])
|
||||
right = build_bvh(block_list[mid:])
|
||||
if left and right:
|
||||
bbox = union_bbox(left.bbox, right.bbox)
|
||||
elif left:
|
||||
bbox = left.bbox
|
||||
else:
|
||||
bbox = right.bbox
|
||||
return BVHNode(bbox, left, right)
|
||||
|
||||
bvh_root = build_bvh(blocks)
|
||||
|
||||
# -----------------------------
|
||||
# BVH Statistics Functions
|
||||
# -----------------------------
|
||||
def get_bvh_stats(node):
|
||||
if node is None:
|
||||
return (0, 0)
|
||||
if node.block is not None:
|
||||
return (1, 1)
|
||||
left_count, left_depth = get_bvh_stats(node.left)
|
||||
right_count, right_depth = get_bvh_stats(node.right)
|
||||
total = 1 + left_count + right_count
|
||||
depth = 1 + max(left_depth, right_depth)
|
||||
return (total, depth)
|
||||
|
||||
# -----------------------------
|
||||
# Ray-AABB Intersection (Slab Method)
|
||||
# -----------------------------
|
||||
def ray_intersect_aabb(origin, direction, bbox):
|
||||
global ray_intersect_count
|
||||
ray_intersect_count += 1
|
||||
|
||||
tmin = -math.inf
|
||||
tmax = math.inf
|
||||
ox, oy = origin
|
||||
dx, dy = direction
|
||||
|
||||
# X slab
|
||||
if dx != 0:
|
||||
tx1 = (bbox[0] - ox) / dx
|
||||
tx2 = (bbox[2] - ox) / dx
|
||||
tmin = max(tmin, min(tx1, tx2))
|
||||
tmax = min(tmax, max(tx1, tx2))
|
||||
else:
|
||||
if not (bbox[0] <= ox <= bbox[2]):
|
||||
return None
|
||||
|
||||
# Y slab
|
||||
if dy != 0:
|
||||
ty1 = (bbox[1] - oy) / dy
|
||||
ty2 = (bbox[3] - oy) / dy
|
||||
tmin = max(tmin, min(ty1, ty2))
|
||||
tmax = min(tmax, max(ty1, ty2))
|
||||
else:
|
||||
if not (bbox[1] <= oy <= bbox[3]):
|
||||
return None
|
||||
|
||||
if tmax >= tmin and tmax >= 0:
|
||||
return tmin if tmin >= 0 else tmax
|
||||
return None
|
||||
|
||||
# -----------------------------
|
||||
# BVH Ray Casting
|
||||
# -----------------------------
|
||||
def ray_cast_bvh(node, origin, direction):
|
||||
if node is None:
|
||||
return None, None
|
||||
t_bbox = ray_intersect_aabb(origin, direction, node.bbox)
|
||||
if t_bbox is None:
|
||||
return None, None
|
||||
|
||||
if node.block is not None:
|
||||
t_hit = ray_intersect_aabb(origin, direction, rect_to_bbox(node.block))
|
||||
if t_hit is not None:
|
||||
return t_hit, node.block
|
||||
else:
|
||||
return None, None
|
||||
|
||||
t_left, block_left = ray_cast_bvh(node.left, origin, direction)
|
||||
t_right, block_right = ray_cast_bvh(node.right, origin, direction)
|
||||
|
||||
if t_left is not None and t_right is not None:
|
||||
return (t_left, block_left) if t_left < t_right else (t_right, block_right)
|
||||
elif t_left is not None:
|
||||
return t_left, block_left
|
||||
elif t_right is not None:
|
||||
return t_right, block_right
|
||||
return None, None
|
||||
|
||||
# -----------------------------
|
||||
# Single Ray Casting (No Bouncing)
|
||||
# -----------------------------
|
||||
def cast_ray_single(origin, direction):
|
||||
"""
|
||||
Cast a single ray from origin in the given direction.
|
||||
If the ray collides with a block within max_distance, return (hit_point, block_color).
|
||||
Otherwise, return None.
|
||||
"""
|
||||
t, hit_block = ray_cast_bvh(bvh_root, origin, direction)
|
||||
if t is None or t > max_distance:
|
||||
return None
|
||||
hit_point = (origin[0] + direction[0] * t,
|
||||
origin[1] + direction[1] * t)
|
||||
block_color = hit_block[1] if isinstance(hit_block, tuple) else (255, 255, 255)
|
||||
return (hit_point, block_color)
|
||||
|
||||
# -----------------------------
|
||||
# Advanced Debug Menu (No Background)
|
||||
# -----------------------------
|
||||
def draw_debug_menu(surface, fps):
|
||||
bvh_node_count, bvh_depth = get_bvh_stats(bvh_root)
|
||||
debug_lines = [
|
||||
"DEBUG MODE ACTIVE",
|
||||
f"FPS: {fps:.1f}",
|
||||
f"Blocks: {len(blocks)}",
|
||||
f"BVH Nodes: {bvh_node_count}",
|
||||
f"BVH Depth: {bvh_depth}",
|
||||
f"Light Source: {light_source}",
|
||||
f"Rays Cast: {num_rays}",
|
||||
f"Ray Intersection Tests (this frame): {ray_intersect_count}",
|
||||
f"Avg Tests per Ray: {ray_intersect_count / num_rays:.2f}",
|
||||
"Toggles: F3 - Debug, R - Regenerate World"
|
||||
]
|
||||
y_offset = 10
|
||||
for line in debug_lines:
|
||||
text_surf = font.render(line, True, (255, 255, 255))
|
||||
surface.blit(text_surf, (10, y_offset))
|
||||
y_offset += text_surf.get_height() + 2
|
||||
|
||||
# -----------------------------
|
||||
# Main Game Loop
|
||||
# -----------------------------
|
||||
light_source = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
|
||||
|
||||
running = True
|
||||
while running:
|
||||
ray_intersect_count = 0 # Reset each frame
|
||||
|
||||
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.key == pygame.K_r:
|
||||
blocks = generate_blocks()
|
||||
bvh_root = build_bvh(blocks)
|
||||
elif event.type == pygame.MOUSEMOTION:
|
||||
light_source = event.pos
|
||||
|
||||
screen.fill((30, 30, 30))
|
||||
|
||||
# Draw voxel blocks in their random colors.
|
||||
# for block in blocks:
|
||||
# pygame.draw.rect(screen, block[1], block[0])
|
||||
|
||||
# For each ray, cast once and mark the collision point with the block's color.
|
||||
for angle in range(0, 360, max(1, 360 // num_rays)):
|
||||
rad = math.radians(angle)
|
||||
direction = (math.cos(rad), math.sin(rad))
|
||||
result = cast_ray_single(light_source, direction)
|
||||
if result is not None:
|
||||
hit_point, hit_color = result
|
||||
# Draw a small circle (pixel) at the collision point.
|
||||
pygame.draw.circle(screen, hit_color, (int(hit_point[0]), int(hit_point[1])), 3)
|
||||
|
||||
# Draw the light source.
|
||||
pygame.draw.circle(screen, (255, 255, 0), light_source, 5)
|
||||
|
||||
if debug_mode:
|
||||
fps = clock.get_fps()
|
||||
draw_debug_menu(screen, fps)
|
||||
|
||||
pygame.display.flip()
|
||||
clock.tick(60)
|
||||
|
||||
pygame.quit()
|
||||
sys.exit()
|
260
lighting/ray-lighting3.py
Normal file
260
lighting/ray-lighting3.py
Normal file
@ -0,0 +1,260 @@
|
||||
import pygame
|
||||
import sys
|
||||
import math
|
||||
import random
|
||||
|
||||
# -----------------------------
|
||||
# Pygame and world initialization
|
||||
# -----------------------------
|
||||
pygame.init()
|
||||
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
|
||||
BLOCK_SIZE = 40
|
||||
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
|
||||
pygame.display.set_caption("2D Voxel Game with Persistent Pixel Lighting")
|
||||
clock = pygame.time.Clock()
|
||||
font = pygame.font.Font(None, 20)
|
||||
|
||||
# -----------------------------
|
||||
# Global Options
|
||||
# -----------------------------
|
||||
debug_mode = False # Toggle full debug overlay (F3)
|
||||
num_rays = 360 # Number of primary rays to cast
|
||||
max_distance = 1000 # Maximum distance for a ray if nothing is hit
|
||||
ray_intersect_count = 0 # Global counter (resets each frame)
|
||||
|
||||
# -----------------------------
|
||||
# Voxel World Generation (each block gets a random color)
|
||||
# -----------------------------
|
||||
def generate_blocks():
|
||||
blocks = []
|
||||
cols = SCREEN_WIDTH // BLOCK_SIZE
|
||||
rows = SCREEN_HEIGHT // BLOCK_SIZE
|
||||
for i in range(cols):
|
||||
for j in range(rows):
|
||||
# 20% chance that this grid cell is a solid block.
|
||||
if random.random() < 0.2:
|
||||
rect = pygame.Rect(i * BLOCK_SIZE, j * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
|
||||
color = (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255))
|
||||
blocks.append((rect, color))
|
||||
return blocks
|
||||
|
||||
blocks = generate_blocks()
|
||||
|
||||
# -----------------------------
|
||||
# BVH Data Structures and Build
|
||||
# -----------------------------
|
||||
class BVHNode:
|
||||
def __init__(self, bbox, left=None, right=None, block=None):
|
||||
self.bbox = bbox # (min_x, min_y, max_x, max_y)
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.block = block # For leaf nodes, store the block (tuple: (pygame.Rect, color))
|
||||
|
||||
def rect_to_bbox(block):
|
||||
rect = block[0] if isinstance(block, tuple) else block
|
||||
return (rect.left, rect.top, rect.right, rect.bottom)
|
||||
|
||||
def union_bbox(b1, b2):
|
||||
x1 = min(b1[0], b2[0])
|
||||
y1 = min(b1[1], b2[1])
|
||||
x2 = max(b1[2], b2[2])
|
||||
y2 = max(b1[3], b2[3])
|
||||
return (x1, y1, x2, y2)
|
||||
|
||||
def build_bvh(block_list):
|
||||
if not block_list:
|
||||
return None
|
||||
if len(block_list) == 1:
|
||||
return BVHNode(rect_to_bbox(block_list[0]), block=block_list[0])
|
||||
block_list.sort(key=lambda block: block[0].centerx)
|
||||
mid = len(block_list) // 2
|
||||
left = build_bvh(block_list[:mid])
|
||||
right = build_bvh(block_list[mid:])
|
||||
if left and right:
|
||||
bbox = union_bbox(left.bbox, right.bbox)
|
||||
elif left:
|
||||
bbox = left.bbox
|
||||
else:
|
||||
bbox = right.bbox
|
||||
return BVHNode(bbox, left, right)
|
||||
|
||||
bvh_root = build_bvh(blocks)
|
||||
|
||||
# -----------------------------
|
||||
# BVH Statistics Functions
|
||||
# -----------------------------
|
||||
def get_bvh_stats(node):
|
||||
if node is None:
|
||||
return (0, 0)
|
||||
if node.block is not None:
|
||||
return (1, 1)
|
||||
left_count, left_depth = get_bvh_stats(node.left)
|
||||
right_count, right_depth = get_bvh_stats(node.right)
|
||||
total = 1 + left_count + right_count
|
||||
depth = 1 + max(left_depth, right_depth)
|
||||
return (total, depth)
|
||||
|
||||
# -----------------------------
|
||||
# Ray-AABB Intersection (Slab Method)
|
||||
# -----------------------------
|
||||
def ray_intersect_aabb(origin, direction, bbox):
|
||||
global ray_intersect_count
|
||||
ray_intersect_count += 1
|
||||
tmin = -math.inf
|
||||
tmax = math.inf
|
||||
ox, oy = origin
|
||||
dx, dy = direction
|
||||
|
||||
# X slab
|
||||
if dx != 0:
|
||||
tx1 = (bbox[0] - ox) / dx
|
||||
tx2 = (bbox[2] - ox) / dx
|
||||
tmin = max(tmin, min(tx1, tx2))
|
||||
tmax = min(tmax, max(tx1, tx2))
|
||||
else:
|
||||
if not (bbox[0] <= ox <= bbox[2]):
|
||||
return None
|
||||
|
||||
# Y slab
|
||||
if dy != 0:
|
||||
ty1 = (bbox[1] - oy) / dy
|
||||
ty2 = (bbox[3] - oy) / dy
|
||||
tmin = max(tmin, min(ty1, ty2))
|
||||
tmax = min(tmax, max(ty1, ty2))
|
||||
else:
|
||||
if not (bbox[1] <= oy <= bbox[3]):
|
||||
return None
|
||||
|
||||
if tmax >= tmin and tmax >= 0:
|
||||
return tmin if tmin >= 0 else tmax
|
||||
return None
|
||||
|
||||
# -----------------------------
|
||||
# BVH Ray Casting
|
||||
# -----------------------------
|
||||
def ray_cast_bvh(node, origin, direction):
|
||||
if node is None:
|
||||
return None, None
|
||||
t_bbox = ray_intersect_aabb(origin, direction, node.bbox)
|
||||
if t_bbox is None:
|
||||
return None, None
|
||||
|
||||
if node.block is not None:
|
||||
t_hit = ray_intersect_aabb(origin, direction, rect_to_bbox(node.block))
|
||||
if t_hit is not None:
|
||||
return t_hit, node.block
|
||||
else:
|
||||
return None, None
|
||||
|
||||
t_left, block_left = ray_cast_bvh(node.left, origin, direction)
|
||||
t_right, block_right = ray_cast_bvh(node.right, origin, direction)
|
||||
|
||||
if t_left is not None and t_right is not None:
|
||||
return (t_left, block_left) if t_left < t_right else (t_right, block_right)
|
||||
elif t_left is not None:
|
||||
return t_left, block_left
|
||||
elif t_right is not None:
|
||||
return t_right, block_right
|
||||
return None, None
|
||||
|
||||
# -----------------------------
|
||||
# Single Ray Casting (No Bouncing)
|
||||
# -----------------------------
|
||||
def cast_ray_single(origin, direction):
|
||||
"""
|
||||
Cast a single ray from origin in the given direction.
|
||||
If the ray collides with a block within max_distance, return (hit_point, block_color).
|
||||
Otherwise, return None.
|
||||
"""
|
||||
t, hit_block = ray_cast_bvh(bvh_root, origin, direction)
|
||||
if t is None or t > max_distance:
|
||||
return None
|
||||
hit_point = (origin[0] + direction[0] * t,
|
||||
origin[1] + direction[1] * t)
|
||||
block_color = hit_block[1] if isinstance(hit_block, tuple) else (255, 255, 255)
|
||||
return (hit_point, block_color)
|
||||
|
||||
# -----------------------------
|
||||
# Persistent Pixel Cache Update
|
||||
# -----------------------------
|
||||
# Create a persistent light map surface once. We fill it with black initially.
|
||||
persistent_light_map = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))
|
||||
persistent_light_map.fill((0, 0, 0))
|
||||
|
||||
def update_pixel_cache(light_pos):
|
||||
"""For each ray cast from light_pos, update the persistent_light_map with the collision pixel and its color."""
|
||||
for angle in range(0, 360, max(1, 360 // num_rays)):
|
||||
rad = math.radians(angle)
|
||||
direction = (math.cos(rad), math.sin(rad))
|
||||
result = cast_ray_single(light_pos, direction)
|
||||
if result is not None:
|
||||
hit_point, hit_color = result
|
||||
# Set the pixel on the persistent_light_map at the collision coordinate.
|
||||
persistent_light_map.set_at((int(hit_point[0]), int(hit_point[1])), hit_color)
|
||||
|
||||
# -----------------------------
|
||||
# Advanced Debug Menu (No Background)
|
||||
# -----------------------------
|
||||
def draw_debug_menu(surface, fps):
|
||||
bvh_node_count, bvh_depth = get_bvh_stats(bvh_root)
|
||||
debug_lines = [
|
||||
"DEBUG MODE ACTIVE",
|
||||
f"FPS: {fps:.1f}",
|
||||
f"Blocks: {len(blocks)}",
|
||||
f"BVH Nodes: {bvh_node_count}",
|
||||
f"BVH Depth: {bvh_depth}",
|
||||
f"Light Source: {light_source}",
|
||||
f"Rays Cast: {num_rays}",
|
||||
f"Ray Intersection Tests (this frame): {ray_intersect_count}",
|
||||
f"Avg Tests per Ray: {ray_intersect_count / num_rays:.2f}",
|
||||
"Toggles: F3 - Debug, R - Regenerate World"
|
||||
]
|
||||
y_offset = 10
|
||||
for line in debug_lines:
|
||||
text_surf = font.render(line, True, (255, 255, 255))
|
||||
surface.blit(text_surf, (10, y_offset))
|
||||
y_offset += text_surf.get_height() + 2
|
||||
|
||||
# -----------------------------
|
||||
# Main Game Loop
|
||||
# -----------------------------
|
||||
light_source = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2) # Starting light source position
|
||||
|
||||
running = True
|
||||
while running:
|
||||
ray_intersect_count = 0 # Reset each frame
|
||||
|
||||
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.key == pygame.K_r:
|
||||
blocks = generate_blocks()
|
||||
bvh_root = build_bvh(blocks)
|
||||
elif event.type == pygame.MOUSEMOTION:
|
||||
light_source = event.pos
|
||||
|
||||
# We don't clear the persistent_light_map so that it accumulates.
|
||||
# Optionally, you could add a slight fade if desired.
|
||||
|
||||
# Update persistent_light_map with new collision pixels for the current light source.
|
||||
update_pixel_cache(light_source)
|
||||
|
||||
# Draw the persistent light map on the main screen.
|
||||
screen.fill((30, 30, 30))
|
||||
screen.blit(persistent_light_map, (0, 0))
|
||||
|
||||
# Draw the light source for reference.
|
||||
pygame.draw.circle(screen, (255, 255, 0), light_source, 5)
|
||||
|
||||
if debug_mode:
|
||||
fps = clock.get_fps()
|
||||
draw_debug_menu(screen, fps)
|
||||
|
||||
pygame.display.flip()
|
||||
clock.tick(60)
|
||||
|
||||
pygame.quit()
|
||||
sys.exit()
|
371
lighting/ray-lighting4.py
Normal file
371
lighting/ray-lighting4.py
Normal file
@ -0,0 +1,371 @@
|
||||
import pygame
|
||||
import sys
|
||||
import math
|
||||
import random
|
||||
|
||||
# -----------------------------
|
||||
# World and Pygame initialization
|
||||
# -----------------------------
|
||||
pygame.init()
|
||||
SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600
|
||||
WORLD_WIDTH, WORLD_HEIGHT = 2000, 2000 # Larger world
|
||||
BLOCK_SIZE = 40
|
||||
|
||||
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
|
||||
pygame.display.set_caption("2D Voxel Game with Debug View Frustum & BVH")
|
||||
clock = pygame.time.Clock()
|
||||
font = pygame.font.Font(None, 20)
|
||||
|
||||
# -----------------------------
|
||||
# Global Options
|
||||
# -----------------------------
|
||||
debug_mode = False # Toggle debug overlay (F3)
|
||||
FOV = 90 # Field of view in degrees for ray casting
|
||||
num_rays = 360 # Number of rays cast within the FOV
|
||||
max_distance = 1000 # Maximum ray distance
|
||||
ray_intersect_count = 0 # Reset each frame
|
||||
|
||||
# -----------------------------
|
||||
# Voxel World Generation (each block gets a random color)
|
||||
# -----------------------------
|
||||
def generate_blocks():
|
||||
blocks = []
|
||||
cols = WORLD_WIDTH // BLOCK_SIZE
|
||||
rows = WORLD_HEIGHT // BLOCK_SIZE
|
||||
for i in range(cols):
|
||||
for j in range(rows):
|
||||
# 20% chance to place a block in this grid cell.
|
||||
if random.random() < 0.2:
|
||||
rect = pygame.Rect(i * BLOCK_SIZE, j * BLOCK_SIZE, BLOCK_SIZE, BLOCK_SIZE)
|
||||
color = (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255))
|
||||
blocks.append((rect, color))
|
||||
return blocks
|
||||
|
||||
blocks = generate_blocks()
|
||||
|
||||
# -----------------------------
|
||||
# BVH Data Structures and Build
|
||||
# -----------------------------
|
||||
class BVHNode:
|
||||
def __init__(self, bbox, left=None, right=None, block=None):
|
||||
self.bbox = bbox # (min_x, min_y, max_x, max_y)
|
||||
self.left = left
|
||||
self.right = right
|
||||
self.block = block # For leaf nodes, store the block (tuple: (pygame.Rect, color))
|
||||
|
||||
def rect_to_bbox(block):
|
||||
rect = block[0] if isinstance(block, tuple) else block
|
||||
return (rect.left, rect.top, rect.right, rect.bottom)
|
||||
|
||||
def union_bbox(b1, b2):
|
||||
return (min(b1[0], b2[0]), min(b1[1], b2[1]),
|
||||
max(b1[2], b2[2]), max(b1[3], b2[3]))
|
||||
|
||||
def build_bvh(block_list):
|
||||
if not block_list:
|
||||
return None
|
||||
if len(block_list) == 1:
|
||||
return BVHNode(rect_to_bbox(block_list[0]), block=block_list[0])
|
||||
block_list.sort(key=lambda block: block[0].centerx)
|
||||
mid = len(block_list) // 2
|
||||
left = build_bvh(block_list[:mid])
|
||||
right = build_bvh(block_list[mid:])
|
||||
if left and right:
|
||||
bbox = union_bbox(left.bbox, right.bbox)
|
||||
elif left:
|
||||
bbox = left.bbox
|
||||
else:
|
||||
bbox = right.bbox
|
||||
return BVHNode(bbox, left, right)
|
||||
|
||||
bvh_root = build_bvh(blocks)
|
||||
|
||||
# -----------------------------
|
||||
# BVH Statistics Functions
|
||||
# -----------------------------
|
||||
def get_bvh_stats(node):
|
||||
if node is None:
|
||||
return (0, 0)
|
||||
if node.block is not None:
|
||||
return (1, 1)
|
||||
left_count, left_depth = get_bvh_stats(node.left)
|
||||
right_count, right_depth = get_bvh_stats(node.right)
|
||||
total = 1 + left_count + right_count
|
||||
depth = 1 + max(left_depth, right_depth)
|
||||
return (total, depth)
|
||||
|
||||
# -----------------------------
|
||||
# Ray-AABB Intersection (Slab Method)
|
||||
# -----------------------------
|
||||
def ray_intersect_aabb(origin, direction, bbox):
|
||||
global ray_intersect_count
|
||||
ray_intersect_count += 1
|
||||
|
||||
tmin = -math.inf
|
||||
tmax = math.inf
|
||||
ox, oy = origin
|
||||
dx, dy = direction
|
||||
|
||||
# X slab
|
||||
if dx != 0:
|
||||
tx1 = (bbox[0] - ox) / dx
|
||||
tx2 = (bbox[2] - ox) / dx
|
||||
tmin = max(tmin, min(tx1, tx2))
|
||||
tmax = min(tmax, max(tx1, tx2))
|
||||
else:
|
||||
if not (bbox[0] <= ox <= bbox[2]):
|
||||
return None
|
||||
|
||||
# Y slab
|
||||
if dy != 0:
|
||||
ty1 = (bbox[1] - oy) / dy
|
||||
ty2 = (bbox[3] - oy) / dy
|
||||
tmin = max(tmin, min(ty1, ty2))
|
||||
tmax = min(tmax, max(ty1, ty2))
|
||||
else:
|
||||
if not (bbox[1] <= oy <= bbox[3]):
|
||||
return None
|
||||
|
||||
if tmax >= tmin and tmax >= 0:
|
||||
return tmin if tmin >= 0 else tmax
|
||||
return None
|
||||
|
||||
# -----------------------------
|
||||
# BVH Ray Casting
|
||||
# -----------------------------
|
||||
def ray_cast_bvh(node, origin, direction):
|
||||
if node is None:
|
||||
return None, None
|
||||
t_bbox = ray_intersect_aabb(origin, direction, node.bbox)
|
||||
if t_bbox is None:
|
||||
return None, None
|
||||
|
||||
if node.block is not None:
|
||||
t_hit = ray_intersect_aabb(origin, direction, rect_to_bbox(node.block))
|
||||
if t_hit is not None:
|
||||
return t_hit, node.block
|
||||
else:
|
||||
return None, None
|
||||
|
||||
t_left, block_left = ray_cast_bvh(node.left, origin, direction)
|
||||
t_right, block_right = ray_cast_bvh(node.right, origin, direction)
|
||||
|
||||
if t_left is not None and t_right is not None:
|
||||
return (t_left, block_left) if t_left < t_right else (t_right, block_right)
|
||||
elif t_left is not None:
|
||||
return t_left, block_left
|
||||
elif t_right is not None:
|
||||
return t_right, block_right
|
||||
return None, None
|
||||
|
||||
# -----------------------------
|
||||
# Single Ray Casting (No Bouncing)
|
||||
# -----------------------------
|
||||
def cast_ray_single(origin, direction):
|
||||
t, hit_block = ray_cast_bvh(bvh_root, origin, direction)
|
||||
if t is None or t > max_distance:
|
||||
return None
|
||||
hit_point = (origin[0] + direction[0] * t,
|
||||
origin[1] + direction[1] * t)
|
||||
block_color = hit_block[1] if isinstance(hit_block, tuple) else (255, 255, 255)
|
||||
return (hit_point, block_color)
|
||||
|
||||
# -----------------------------
|
||||
# Persistent Light Map Setup (World-Sized Surface with Per-Pixel Alpha)
|
||||
# -----------------------------
|
||||
persistent_light_map = pygame.Surface((WORLD_WIDTH, WORLD_HEIGHT), pygame.SRCALPHA)
|
||||
persistent_light_map.fill((0, 0, 0, 255)) # Start fully black (opaque)
|
||||
|
||||
def fade_light_map():
|
||||
# Multiply each pixel by ~254/255 (about 0.99608) per frame.
|
||||
persistent_light_map.fill((254, 254, 254, 254), special_flags=pygame.BLEND_RGBA_MULT)
|
||||
|
||||
# -----------------------------
|
||||
# Update the Light Map from the Current Light Source within a FOV
|
||||
# -----------------------------
|
||||
def update_light_map(light_pos, base_angle):
|
||||
angle_step = FOV / num_rays
|
||||
for i in range(num_rays):
|
||||
angle = base_angle - FOV / 2 + i * angle_step
|
||||
rad = math.radians(angle)
|
||||
direction = (math.cos(rad), math.sin(rad))
|
||||
result = cast_ray_single(light_pos, direction)
|
||||
if result is not None:
|
||||
hit_point, hit_color = result
|
||||
persistent_light_map.set_at((int(hit_point[0]), int(hit_point[1])), hit_color)
|
||||
|
||||
# -----------------------------
|
||||
# Debug Draw Functions
|
||||
# -----------------------------
|
||||
def draw_bvh(node, surface, cam_offset):
|
||||
if node is None:
|
||||
return
|
||||
x, y, x2, y2 = node.bbox
|
||||
rect = pygame.Rect(x - cam_offset[0], y - cam_offset[1], x2 - x, y2 - y)
|
||||
color = (0, 255, 0) if node.block is not None else (255, 0, 0)
|
||||
pygame.draw.rect(surface, color, rect, 1)
|
||||
draw_bvh(node.left, surface, cam_offset)
|
||||
draw_bvh(node.right, surface, cam_offset)
|
||||
|
||||
def draw_view_frustum(light_pos, base_angle, cam_offset):
|
||||
# Compute left and right boundary rays
|
||||
left_angle = math.radians(base_angle - FOV / 2)
|
||||
right_angle = math.radians(base_angle + FOV / 2)
|
||||
end_left = (light_pos[0] + math.cos(left_angle) * max_distance,
|
||||
light_pos[1] + math.sin(left_angle) * max_distance)
|
||||
end_right = (light_pos[0] + math.cos(right_angle) * max_distance,
|
||||
light_pos[1] + math.sin(right_angle) * max_distance)
|
||||
# Convert to screen coords
|
||||
lp_screen = (light_pos[0] - cam_offset[0], light_pos[1] - cam_offset[1])
|
||||
el_screen = (end_left[0] - cam_offset[0], end_left[1] - cam_offset[1])
|
||||
er_screen = (end_right[0] - cam_offset[0], end_right[1] - cam_offset[1])
|
||||
# Draw boundary rays
|
||||
pygame.draw.line(screen, (0, 255, 255), lp_screen, el_screen, 1)
|
||||
pygame.draw.line(screen, (0, 255, 255), lp_screen, er_screen, 1)
|
||||
# Optionally draw the frustum polygon
|
||||
pygame.draw.polygon(screen, (0, 255, 255, 50), [lp_screen, el_screen, er_screen], 1)
|
||||
|
||||
def debug_draw_rays(light_pos, base_angle, cam_offset):
|
||||
angle_step = FOV / num_rays
|
||||
for i in range(num_rays):
|
||||
angle = base_angle - FOV / 2 + i * angle_step
|
||||
rad = math.radians(angle)
|
||||
direction = (math.cos(rad), math.sin(rad))
|
||||
result = cast_ray_single(light_pos, direction)
|
||||
if result is not None:
|
||||
hit_point, _ = result
|
||||
start = (light_pos[0] - cam_offset[0], light_pos[1] - cam_offset[1])
|
||||
end = (hit_point[0] - cam_offset[0], hit_point[1] - cam_offset[1])
|
||||
else:
|
||||
# No hit; extend ray to max_distance.
|
||||
end_pt = (light_pos[0] + direction[0] * max_distance,
|
||||
light_pos[1] + direction[1] * max_distance)
|
||||
start = (light_pos[0] - cam_offset[0], light_pos[1] - cam_offset[1])
|
||||
end = (end_pt[0] - cam_offset[0], end_pt[1] - cam_offset[1])
|
||||
pygame.draw.line(screen, (255, 255, 255), start, end, 1)
|
||||
|
||||
# -----------------------------
|
||||
# Advanced Debug Menu (No Background)
|
||||
# -----------------------------
|
||||
def draw_debug_menu(surface, fps):
|
||||
bvh_node_count, bvh_depth = get_bvh_stats(bvh_root)
|
||||
debug_lines = [
|
||||
"DEBUG MODE ACTIVE",
|
||||
f"FPS: {fps:.1f}",
|
||||
f"Blocks: {len(blocks)}",
|
||||
f"BVH Nodes: {bvh_node_count}",
|
||||
f"BVH Depth: {bvh_depth}",
|
||||
f"Player Pos: ({player_rect.centerx}, {player_rect.centery})",
|
||||
f"Rays Cast (FOV): {num_rays}",
|
||||
f"Ray Intersection Tests: {ray_intersect_count}",
|
||||
f"Avg Tests per Ray: {ray_intersect_count / num_rays:.2f}",
|
||||
"Toggles: F3 - Debug, R - Regenerate World"
|
||||
]
|
||||
y_offset = 10
|
||||
for line in debug_lines:
|
||||
text_surf = font.render(line, True, (255, 255, 255))
|
||||
surface.blit(text_surf, (10, y_offset))
|
||||
y_offset += text_surf.get_height() + 2
|
||||
|
||||
# -----------------------------
|
||||
# Player Setup and Collision
|
||||
# -----------------------------
|
||||
player_speed = 4
|
||||
player_rect = pygame.Rect(WORLD_WIDTH // 2, WORLD_HEIGHT // 2, 20, 20)
|
||||
|
||||
def move_player(dx, dy):
|
||||
player_rect.x += dx
|
||||
for block, _ in blocks:
|
||||
if player_rect.colliderect(block):
|
||||
if dx > 0:
|
||||
player_rect.right = block.left
|
||||
elif dx < 0:
|
||||
player_rect.left = block.right
|
||||
player_rect.y += dy
|
||||
for block, _ in blocks:
|
||||
if player_rect.colliderect(block):
|
||||
if dy > 0:
|
||||
player_rect.bottom = block.top
|
||||
elif dy < 0:
|
||||
player_rect.top = block.bottom
|
||||
|
||||
# -----------------------------
|
||||
# Camera Setup
|
||||
# -----------------------------
|
||||
def get_camera_offset():
|
||||
cam_x = player_rect.centerx - SCREEN_WIDTH // 2
|
||||
cam_y = player_rect.centery - SCREEN_HEIGHT // 2
|
||||
return cam_x, cam_y
|
||||
|
||||
# -----------------------------
|
||||
# Main Game Loop
|
||||
# -----------------------------
|
||||
running = True
|
||||
while running:
|
||||
ray_intersect_count = 0 # Reset each frame
|
||||
|
||||
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.key == pygame.K_r:
|
||||
blocks = generate_blocks()
|
||||
bvh_root = build_bvh(blocks)
|
||||
|
||||
# Process player movement.
|
||||
keys = pygame.key.get_pressed()
|
||||
dx = dy = 0
|
||||
if keys[pygame.K_LEFT] or keys[pygame.K_a]:
|
||||
dx = -player_speed
|
||||
if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
|
||||
dx = player_speed
|
||||
if keys[pygame.K_UP] or keys[pygame.K_w]:
|
||||
dy = -player_speed
|
||||
if keys[pygame.K_DOWN] or keys[pygame.K_s]:
|
||||
dy = player_speed
|
||||
move_player(dx, dy)
|
||||
|
||||
# Light source is the player's center.
|
||||
light_source = player_rect.center
|
||||
camera_offset = get_camera_offset()
|
||||
|
||||
# Compute the world coordinate of the mouse cursor.
|
||||
mouse_screen = pygame.mouse.get_pos()
|
||||
mouse_world = (mouse_screen[0] + camera_offset[0], mouse_screen[1] + camera_offset[1])
|
||||
delta_x = mouse_world[0] - light_source[0]
|
||||
delta_y = mouse_world[1] - light_source[1]
|
||||
base_angle = math.degrees(math.atan2(delta_y, delta_x))
|
||||
|
||||
# Fade the persistent light map.
|
||||
fade_light_map()
|
||||
# Update the persistent light map with new collision pixels (only within the FOV).
|
||||
update_light_map(light_source, base_angle)
|
||||
|
||||
# Clear the screen.
|
||||
screen.fill((30, 30, 30))
|
||||
# Blit the persistent light map with camera offset.
|
||||
screen.blit(persistent_light_map, (-camera_offset[0], -camera_offset[1]))
|
||||
|
||||
# Draw the player.
|
||||
pygame.draw.rect(screen, (255, 255, 0),
|
||||
(player_rect.x - camera_offset[0], player_rect.y - camera_offset[1],
|
||||
player_rect.width, player_rect.height))
|
||||
|
||||
if debug_mode:
|
||||
# Draw BVH visualizer.
|
||||
draw_bvh(bvh_root, screen, camera_offset)
|
||||
# Draw the view frustum.
|
||||
draw_view_frustum(light_source, base_angle, camera_offset)
|
||||
# Draw each ray cast within the FOV.
|
||||
debug_draw_rays(light_source, base_angle, camera_offset)
|
||||
# Draw debug menu.
|
||||
fps = clock.get_fps()
|
||||
draw_debug_menu(screen, fps)
|
||||
|
||||
pygame.display.flip()
|
||||
clock.tick(60)
|
||||
|
||||
pygame.quit()
|
||||
sys.exit()
|
Loading…
Reference in New Issue
Block a user