Skip to content

Instantly share code, notes, and snippets.

@paulnbrd
Last active March 6, 2021 21:56
Show Gist options
  • Save paulnbrd/d6ac6f948e58e001e4d423f035e664d8 to your computer and use it in GitHub Desktop.
Save paulnbrd/d6ac6f948e58e001e4d423f035e664d8 to your computer and use it in GitHub Desktop.
import pygame, sys
pygame.init()
import shadow
polygons = [
(
(75, 150),
(60, 175),
(75, 350),
(300, 225),
(75, 150)
),
(
(88*2, 198*2),
(75*2, 210*2),
(100*2, 350*2),
(300*2, 225*2),
(100*2, 150*2)
),
(
(530, 250),
(708, 193),
(627, 300)
),
(
(530*2, 250*2),
(708*2, 193*2),
(627*2, 300*2)
),
]
root = pygame.display.set_mode((1920, 1080), pygame.DOUBLEBUF | pygame.HWACCEL | pygame.HWSURFACE)
done = False
shadow_engine = shadow.Engine(polygons, cast_number=100, optimized=True)
clock = pygame.time.Clock()
font = pygame.font.SysFont("Arial Black", 12)
debug_mode: bool = True
while not done:
_delta = clock.tick(0)
_t = str(round(clock.get_fps())) + " FPS (" + str(_delta) + "ms)"
# pygame.display.set_caption(_t)
for event in pygame.event.get():
if event.type == pygame.QUIT or event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit(0)
elif event.type == pygame.KEYDOWN and event.key == pygame.K_e:
debug_mode = not debug_mode
root.fill((0, 0, 0))
for points in polygons:
pygame.draw.polygon(root, (0, 255, 255), points, 0)
shadow_engine.set_pos(pygame.mouse.get_pos())
shadow_engine.update()
if debug_mode:
shadow_engine.debug_draw(root)
else:
p = shadow_engine.get_shadow_polygon()
pygame.draw.polygon(root, (255, 255, 255), p)
t = font.render("Press e to switch between debug mode and render mode", True, (255, 0, 255))
root.blit(t, (0, 0))
t = font.render(str(len(shadow_engine.casts)) + " casts - "+_t, True, (255, 0, 255))
root.blit(t, (0, t.get_height()))
pygame.display.flip()
import pygame
import math
from typing import List, Tuple
import logging
import math
import random
from operator import attrgetter
pygame.init()
logging.basicConfig(level="DEBUG") # Not used...
def line_intersect(Ax1, Ay1, Ax2, Ay2, Bx1, By1, Bx2, By2):
""" returns a (x, y) tuple or False if there is no intersection """
d = (By2 - By1) * (Ax2 - Ax1) - (Bx2 - Bx1) * (Ay2 - Ay1)
if d:
uA = ((Bx2 - Bx1) * (Ay1 - By1) - (By2 - By1) * (Ax1 - Bx1)) / d
uB = ((Ax2 - Ax1) * (Ay1 - By1) - (Ay2 - Ay1) * (Ax1 - Bx1)) / d
else:
return False
if not (0 <= uA <= 1 and 0 <= uB <= 1):
return False
x = Ax1 + uA * (Ax2 - Ax1)
y = Ay1 + uA * (Ay2 - Ay1)
return x, y
def _line_intersection(line1, line2):
""" Simplification of line_intersect """
return line_intersect(line1[0][0], line1[0][1], line1[1][0], line1[1][1], line2[0][0], line2[0][1], line2[1][0],
line2[1][1])
####################
class Cast:
def __init__(
self,
angle: float,
start_pos: Tuple[float, float],
polygons: List[Tuple],
length: float = 750 * 1.5,
screen_size: Tuple[int, int] = (750, 750)
):
"""
A cast
:param angle: The angle (in radians) of the cast
:param start_pos: The start pos
:param polygons: The polygons of the 'map'
:param length: The max length of the cast
:param screen_size: The surface/screen size
"""
self.angle = angle
self.polygons = polygons
self.cast_length = length
self.intersect_pos = (0, 0)
self._screen_size = screen_size
self.end_pos_max = None
self.start_pos = None
self.set_pos(start_pos)
self.has_intersected: bool = False
def set_position(self, *a, **k):
return self.set_pos(*a, **k)
def set_pos(self, newpos):
self.start_pos = newpos
self.end_pos_max = math.cos(self.angle) * self.cast_length + self.start_pos[0], math.sin(
self.angle) * self.cast_length + self.start_pos[1]
# if self.end_pos_max[0] > self._screen_size[0]:
# self.end_pos_max = self._screen_size[0], self.end_pos_max[1]
# elif self.end_pos_max[0] < 0:
# self.end_pos_max = 0, self.end_pos_max[1]
# if self.end_pos_max[1] > self._screen_size[1]:
# self.end_pos_max = self.end_pos_max[0], self._screen_size[1]
# elif self.end_pos_max[1] < 0:
# self.end_pos_max = self.end_pos_max[0], 0
# Not used, but to optimize: the cast should stop on the screen/surface borders
def cast(self):
closest = False
intersect = None
for polygon in self.polygons:
for i in range(len(polygon)): # For each points of the polygons
# This loop compute the closest intersection point
if i == len(polygon) - 1:
tmp = _line_intersection((polygon[0], polygon[i]),
(self.start_pos, self.end_pos_max))
else:
tmp = _line_intersection((polygon[i], polygon[i + 1]),
(self.start_pos, self.end_pos_max))
if tmp:
dx = tmp[0] - self.start_pos[0]
dy = tmp[1] - self.start_pos[1]
distance = math.sqrt(dx ** 2 + dy ** 2) # Compute angle
if not closest:
closest = distance
intersect = tmp
elif distance < closest:
closest = distance
intersect = tmp
self.intersect_pos = intersect
if not self.intersect_pos:
self.has_intersected = False
self.intersect_pos = self.end_pos_max
else:
self.has_intersected = True
return self.intersect_pos # This code is right, I think
class Engine:
def __init__(
self,
polygons: List[tuple],
optimized: bool = True,
pos: Tuple[float, float] = pygame.mouse.get_pos(),
cast_number: int = 360
):
"""
The shadow engine
:param polygons: The polygons, represented as tuple in a list of lists
:param optimized: Is the engine optimized (just for testing purposes, it should always be True)
:param pos: The engine pos, from where the shadow come from
:param cast_number: The number of cast if not optimized, else the number of additional casts
"""
self.polygons = polygons
self.optimized = optimized
self.pos = pos
self.casts = []
self.additional_cast_number = cast_number
# if self.additional_cast_number < 25:
# self.additional_cast_number = 25
# logging.debug("You need at least 50 additional casts for a good rendering")
if not self.optimized:
for i in range(self.additional_cast_number):
self.casts.append(Cast(
i / self.additional_cast_number * 360,
self.pos,
self.polygons
))
def set_pos(self, newpos):
""" Change engine position, and all casts position too """
self.pos = newpos
for cast in self.casts:
cast.set_pos(newpos)
def update(self):
if self.optimized:
self.casts.clear()
####
# Additional casts
nb = self.additional_cast_number
for i in range(nb):
self.casts.append(Cast(
i / nb * 2 * math.pi,
self.pos,
self.polygons
))
####
for polygon in self.polygons:
for point in polygon: # For each points
dx = point[0] - self.pos[0]
dy = point[1] - self.pos[1]
angle = math.atan2(dy, dx) # Compute angle
# Add cast on the point, just before, and just after
self.casts.append(Cast(
angle - 0.001,
self.pos,
self.polygons
))
self.casts.append(Cast(
angle,
self.pos,
self.polygons
))
self.casts.append(Cast(
angle + 0.001,
self.pos,
self.polygons
))
for cast in self.casts:
cast.cast() # Cast all casts
#############
## I THINK THIS IS THE PROBLEM (or something like that) ! ##
#############
self.casts = sorted(self.casts, key=lambda x: x.angle) # Sort by angle
def get_shadow_polygon(self):
""" Returns the polygon from each casts position """
if self.optimized:
o = []
c = self.casts.copy()
for i in range(len(c)):
m = min(c, key=attrgetter("angle"))
o.append(m)
c.remove(m)
else:
o = self.casts
returnarray = []
for i in o:
returnarray.append(i.intersect_pos)
return returnarray
def debug_draw(self, surface: pygame.Surface):
""" Debug draw of casts """
font = pygame.font.SysFont("Arial Black", 12)
for cast in self.casts:
pygame.draw.line(surface, (0, 0, 255) if cast.has_intersected else (255, 0, 0), cast.start_pos,
cast.intersect_pos)
# text = font.render(str(self.casts.index(cast)), True, (255, 255, 255))
text = font.render(str(round(cast.angle, 2)), True, (255, 255, 255))
surface.blit(text, cast.intersect_pos)
text = font.render(str(self.casts.index(cast)), True, (255, 255, 255))
surface.blit(text,
(cast.intersect_pos[0],
cast.intersect_pos[1] + text.get_height() + 2)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment