From 75a6a3c70682e0e4bf4145e772bf6724c408257c Mon Sep 17 00:00:00 2001 From: OusmBlueNinja <89956790+OusmBlueNinja@users.noreply.github.com> Date: Sat, 5 Apr 2025 18:26:40 -0500 Subject: [PATCH] ray-cast stuff --- bounding-box-thing/bvh.py | 115 +++++++++ bounding-box-thing/main.py | 493 ++++++++++++++++++++++++++++++++++++ bounding-box-thing/test2.py | 83 ++++++ lighting/ray-lighting.py | 299 ++++++++++++++++++++++ lighting/ray-lighting2.py | 252 ++++++++++++++++++ lighting/ray-lighting3.py | 260 +++++++++++++++++++ lighting/ray-lighting4.py | 371 +++++++++++++++++++++++++++ 7 files changed, 1873 insertions(+) create mode 100644 bounding-box-thing/bvh.py create mode 100644 bounding-box-thing/main.py create mode 100644 bounding-box-thing/test2.py create mode 100644 lighting/ray-lighting.py create mode 100644 lighting/ray-lighting2.py create mode 100644 lighting/ray-lighting3.py create mode 100644 lighting/ray-lighting4.py diff --git a/bounding-box-thing/bvh.py b/bounding-box-thing/bvh.py new file mode 100644 index 0000000..d598daf --- /dev/null +++ b/bounding-box-thing/bvh.py @@ -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() diff --git a/bounding-box-thing/main.py b/bounding-box-thing/main.py new file mode 100644 index 0000000..7eef282 --- /dev/null +++ b/bounding-box-thing/main.py @@ -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() diff --git a/bounding-box-thing/test2.py b/bounding-box-thing/test2.py new file mode 100644 index 0000000..e990993 --- /dev/null +++ b/bounding-box-thing/test2.py @@ -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) diff --git a/lighting/ray-lighting.py b/lighting/ray-lighting.py new file mode 100644 index 0000000..9c48cd5 --- /dev/null +++ b/lighting/ray-lighting.py @@ -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() diff --git a/lighting/ray-lighting2.py b/lighting/ray-lighting2.py new file mode 100644 index 0000000..3cab704 --- /dev/null +++ b/lighting/ray-lighting2.py @@ -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() diff --git a/lighting/ray-lighting3.py b/lighting/ray-lighting3.py new file mode 100644 index 0000000..06b13ce --- /dev/null +++ b/lighting/ray-lighting3.py @@ -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() diff --git a/lighting/ray-lighting4.py b/lighting/ray-lighting4.py new file mode 100644 index 0000000..6631cd6 --- /dev/null +++ b/lighting/ray-lighting4.py @@ -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()