Last active
March 25, 2016 11:03
-
-
Save marcusmoller/9538871 to your computer and use it in GitHub Desktop.
SIGHT & LIGHT demo in Python/PyGame
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
SIGHT & LIGHT by ncase (https://github.com/ncase/sight-and-light) | |
Ported to Python/PyGame by Marcus Møller (https://github.com/marcusmoller) | |
''' | |
import pygame | |
import pygame.gfxdraw | |
import math | |
class SightAndLight(): | |
def __init__(self): | |
# initialize pygame | |
pygame.init() | |
self.screen = pygame.display.set_mode((640, 360)) | |
pygame.display.set_caption("SIGHT & LIGHT Demo - Python/PyGame") | |
# mouse position | |
self.mouse_pos = (0, 0) | |
# general | |
self.fps_clock = pygame.time.Clock() | |
self.running = True | |
self.update_shadows = True # only call update on mouse movement | |
# segments | |
self.segments = [ | |
# Border | |
{"a":{"x":0,"y":0}, "b":{"x":640,"y":0}}, | |
{"a":{"x":640,"y":0}, "b":{"x":640,"y":360}}, | |
{"a":{"x":640,"y":360}, "b":{"x":0,"y":360}}, | |
{"a":{"x":0,"y":360}, "b":{"x":0,"y":0}}, | |
# Polygon #1 | |
{"a":{"x":100,"y":150}, "b":{"x":120,"y":50}}, | |
{"a":{"x":120,"y":50}, "b":{"x":200,"y":80}}, | |
{"a":{"x":200,"y":80}, "b":{"x":140,"y":210}}, | |
{"a":{"x":140,"y":210}, "b":{"x":100,"y":150}}, | |
# Polygon #2 | |
{"a":{"x":100,"y":200}, "b":{"x":120,"y":250}}, | |
{"a":{"x":120,"y":250}, "b":{"x":60,"y":300}}, | |
{"a":{"x":60,"y":300}, "b":{"x":100,"y":200}}, | |
# Polygon #3 | |
{"a":{"x":200,"y":260}, "b":{"x":220,"y":150}}, | |
{"a":{"x":220,"y":150}, "b":{"x":300,"y":200}}, | |
{"a":{"x":300,"y":200}, "b":{"x":350,"y":320}}, | |
{"a":{"x":350,"y":320}, "b":{"x":200,"y":260}}, | |
# Polygon #4 | |
{"a":{"x":340,"y":60}, "b":{"x":360,"y":40}}, | |
{"a":{"x":360,"y":40}, "b":{"x":370,"y":70}}, | |
{"a":{"x":370,"y":70}, "b":{"x":340,"y":60}}, | |
# Polygon #5 | |
{"a":{"x":450,"y":190}, "b":{"x":560,"y":170}}, | |
{"a":{"x":560,"y":170}, "b":{"x":540,"y":270}}, | |
{"a":{"x":540,"y":270}, "b":{"x":430,"y":290}}, | |
{"a":{"x":430,"y":290}, "b":{"x":450,"y":190}}, | |
# Polygon #6 | |
{"a":{"x":400,"y":95}, "b":{"x":580,"y":50}}, | |
{"a":{"x":580,"y":50}, "b":{"x":480,"y":150}}, | |
{"a":{"x":480,"y":150}, "b":{"x":400,"y":95}} | |
] | |
# Intersects | |
self.intersects = [] | |
# Points | |
self.points = [] | |
def run(self): | |
while self.running: | |
self.main_loop() | |
def main_loop(self): | |
self.handle_input() | |
if self.update_shadows: | |
self.update() | |
self.update_shadows = False | |
self.render_frame() | |
def update(self): | |
# Clear old points | |
self.points = [] | |
# Get all unique points | |
for segment in self.segments: | |
self.points.append((segment['a'], segment['b'])) | |
unique_points = [] | |
for point in self.points: | |
if point not in unique_points: | |
unique_points.append(point) | |
# Get all angles | |
unique_angles = [] | |
for point in unique_points: | |
angle = math.atan2(point[0]["y"]-self.mouse_pos[1], point[0]["x"]-self.mouse_pos[0]) | |
point[0]["angle"] = angle | |
unique_angles.append(angle-0.00001) | |
unique_angles.append(angle) | |
unique_angles.append(angle+0.00001) | |
# RAYS IN ALL DIRECTIONS | |
self.intersects = [] | |
for angle in unique_angles: | |
# Calculate dx & dy from angle | |
dx = math.cos(angle) | |
dy = math.sin(angle) | |
# Ray from center of screen to mouse | |
ray = { | |
"a": {"x":self.mouse_pos[0], "y": self.mouse_pos[1]}, | |
"b": {"x": self.mouse_pos[0]+dx, "y": self.mouse_pos[1]+dy} | |
} | |
# Find CLOSEST intersection | |
closest_intersect = None | |
for segment in self.segments: | |
intersect = self.get_intersection(ray, segment) | |
if not intersect: continue | |
if not closest_intersect or intersect["param"] < closest_intersect["param"]: | |
closest_intersect = intersect | |
# Intersect angle | |
if not closest_intersect: continue | |
closest_intersect["angle"] = angle | |
# Add to list of intersects | |
self.intersects.append(closest_intersect) | |
# Sort intersects by angle | |
self.intersects = sorted(self.intersects, key=lambda k: k['angle']) | |
def render_frame(self): | |
self.screen.fill((255, 255, 255)) | |
# draw segments | |
for segment in self.segments: | |
pygame.draw.aaline(self.screen, (153, 153, 153), (segment['a']['x'], segment['a']['y']), (segment['b']['x'], segment['b']['y'])) | |
self.draw_polygon(self.intersects, (221, 56, 56)) | |
# draw debug lines | |
for intersect in self.intersects: | |
pygame.draw.aaline(self.screen, (255, 85, 85), self.mouse_pos, (intersect['x'], intersect['y'])) | |
# limit fps | |
self.fps_clock.tick(60) | |
# update screen | |
pygame.display.update() | |
def handle_input(self): | |
pygame.event.pump() | |
for event in pygame.event.get(): | |
if event.type == pygame.QUIT: | |
self.running = False | |
print("quit") | |
break | |
# KEYBOARD | |
elif event.type == pygame.KEYDOWN: | |
if event.key == pygame.K_ESCAPE or event.key == pygame.K_q: | |
self.running = False | |
# MOUSE | |
elif event.type == pygame.MOUSEMOTION: | |
self.mouse_pos = event.pos | |
self.update_shadows = True | |
def get_intersection(self, ray, segment): | |
''' Find intersection of RAY & SEGMENT ''' | |
# RAY in parametric: Point + Direction*T1 | |
r_px = ray['a']['x'] | |
r_py = ray['a']['y'] | |
r_dx = ray['b']['x'] - ray['a']['x'] | |
r_dy = ray['b']['y'] - ray['a']['y'] | |
# SEGMENT in parametric: Point + Direction*T2 | |
s_px = segment['a']['x'] | |
s_py = segment['a']['y'] | |
s_dx = segment['b']['x'] - segment['a']['x'] | |
s_dy = segment['b']['y'] - segment['a']['y'] | |
# Are they parallel? If so, no intersect | |
r_mag = math.sqrt(r_dx*r_dx+r_dy*r_dy) | |
s_mag = math.sqrt(s_dx*s_dx+s_dy*s_dy) | |
if r_dx/r_mag == s_dx/s_mag and r_dy/r_mag == s_dy/s_mag: | |
return None | |
# SOLVE FOR T1 & T2 | |
# r_px+r_dx*T1 = s_px+s_dx*T2 && r_py+r_dy*T1 = s_py+s_dy*T2 | |
# ==> T1 = (s_px+s_dx*T2-r_px)/r_dx = (s_py+s_dy*T2-r_py)/r_dy | |
# ==> s_px*r_dy + s_dx*T2*r_dy - r_px*r_dy = s_py*r_dx + s_dy*T2*r_dx - r_py*r_dx | |
# ==> T2 = (r_dx*(s_py-r_py) + r_dy*(r_px-s_px))/(s_dx*r_dy - s_dy*r_dx) | |
# todo: fix zerodivision error handling | |
try: | |
T2 = (r_dx*(s_py-r_py) + r_dy*(r_px-s_px))/(s_dx*r_dy - s_dy*r_dx) | |
except ZeroDivisionError: | |
T2 = (r_dx*(s_py-r_py) + r_dy*(r_px-s_px))/(s_dx*r_dy - s_dy*r_dx-0.01) | |
try: | |
T1 = (s_px+s_dx*T2-r_px)/r_dx | |
except ZeroDivisionError: | |
T1 = (s_px+s_dx*T2-r_px)/(r_dx-0.01) | |
# Must be within parametic whatevers for RAY/SEGMENT | |
if T1 < 0: return None | |
if T2 < 0 or T2>1: return None | |
# Return the POINT OF INTERSECTION | |
return { | |
"x": r_px+r_dx*T1, | |
"y": r_py+r_dy*T1, | |
"param": T1 | |
} | |
def draw_polygon(self, polygon, color): | |
# collect coordinates for a giant polygon | |
points = [] | |
for intersect in polygon: | |
points.append((intersect['x'], intersect['y'])) | |
# draw as a giant polygon | |
pygame.gfxdraw.aapolygon(self.screen, points, color) | |
pygame.gfxdraw.filled_polygon(self.screen, points, color) | |
if __name__ == "__main__": | |
demo = SightAndLight() | |
demo.run() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Marcus,
I found this while searching for a similar function. My question is, how would one add multiple points of light?
Thank you!
Aaron