Better readability for SAT demo

This commit is contained in:
Matt Low 2020-11-16 08:55:33 +04:00
parent 36ce94d3de
commit 72860a3e6b

View File

@ -6,6 +6,40 @@ from ..math import *
from ..draw import * from ..draw import *
def compute_separating_axes(p1, p2):
"""Compute the projections upon the separating axes of polygons p1 and p2.
For efficient collision detection, this could early-exit as soon as one
axis is found where the projections of p1 and p2 do not overlap. For
visualization purposes, we compute the projections for all axes and
return the results."""
# The list of axes to project upon
axes = []
# Iterate the normals of both polygons
for normal in p1.get_normals() + p2.get_normals():
# we can trim out normals that are in the same or opposite direction
# as axes we already have, since they will give us the same overlap
# information
if not any(is_colinear(*normal, *axis) for axis in axes):
axes.append(normal)
# helper function to find the min/max projections of vertices onto
# the given axis.
def get_min_max(axis, vertices):
projections = [dot(*axis, *vertex) for vertex in vertices]
return min(projections), max(projections)
return [
(axis,
get_min_max(axis, p1.get_translated_vertices()),
get_min_max(axis, p2.get_translated_vertices()))
for axis in axes
]
class SeparatingAxisTheorem: class SeparatingAxisTheorem:
title = "Separating Axis Theorem" title = "Separating Axis Theorem"
@ -19,6 +53,7 @@ class SeparatingAxisTheorem:
self.generate_shape2() self.generate_shape2()
def generate_shape2(self): def generate_shape2(self):
"""Generates the shape we can move around - a regular polygon."""
rotation = 45 if self.shape2 is None else self.shape2.rotation rotation = 45 if self.shape2 is None else self.shape2.rotation
self.shape2 = generate_polygon(*get_cursor_pos(), self.shape2 = generate_polygon(*get_cursor_pos(),
rotation, rotation,
@ -27,9 +62,11 @@ class SeparatingAxisTheorem:
def handle_key_down(self, key): def handle_key_down(self, key):
if key == pygame.K_a: if key == pygame.K_a:
# remove a side from the polygon
self.shape2_sides = max(self.shape2_sides - 1, 3) self.shape2_sides = max(self.shape2_sides - 1, 3)
self.generate_shape2() self.generate_shape2()
if key == pygame.K_d: if key == pygame.K_d:
# add a side to the polygon
self.shape2_sides = min(self.shape2_sides + 1, 31) self.shape2_sides = min(self.shape2_sides + 1, 31)
self.generate_shape2() self.generate_shape2()
@ -38,77 +75,65 @@ class SeparatingAxisTheorem:
inputs = get_inputs() inputs = get_inputs()
if inputs[pygame.K_q]: if inputs[pygame.K_q]:
# rotate counter-clockwise by 90 degrees every second
self.shape2.rotate((1/60) * -90) self.shape2.rotate((1/60) * -90)
if inputs[pygame.K_e]: if inputs[pygame.K_e]:
# same as above, but clockwise
self.shape2.rotate((1/60) * 90) self.shape2.rotate((1/60) * 90)
def draw_axis(self, surface, normal, overlap, min1, max1, min2, max2):
offset = scale(*rnormal(*normal), 300)
colliding = overlap < 0
if not colliding:
# separating line
dist = scale(*normal,
(min1 if min1 > min2 else min2) - overlap * 0.5)
line(surface, CYAN,
add(*dist, *offset),
add(*dist, *scale(*lnormal(*normal), 1000)))
# axis
line(surface, WHITE,
add(*scale(*reverse(*normal), 1000), *offset),
add(*scale(*normal, 1000), *offset))
# shape 1
line(surface, GREEN if not colliding else RED,
add(*scale(*normal, min1), *offset),
add(*scale(*normal, max1), *offset), 8)
# shape 2
line(surface, GREEN if not colliding else RED,
add(*scale(*normal, min2), *offset),
add(*scale(*normal, max2), *offset), 8)
def render(self, surface): def render(self, surface):
self.handle_input() self.handle_input()
normals = [] gaps = []
def add_if_not_exists(normal): for result in compute_separating_axes(self.shape1, self.shape2):
for existing in normals:
if is_colinear(*existing, *normal):
return
normals.append(normal)
for normal in self.shape1.get_normals() + self.shape2.get_normals(): axis, (min1, max1), (min2, max2) = result
add_if_not_exists(normal)
def get_min_max(normal, vertices): # length of the gap between the two projections
_min = float('inf') gap = max(min2 - max1, min1 - max2)
_max = float('-inf') gaps.append(gap)
for vertex in vertices:
proj = dot(*normal, *vertex)
_min = min(_min, proj)
_max = max(_max, proj)
return _min, _max
overlaps = [] self.draw_axis(surface, axis, gap, min1, max1, min2, max2)
for normal in normals:
min1, max1 = get_min_max(normal,
self.shape1.get_translated_vertices())
min2, max2 = get_min_max(normal,
self.shape2.get_translated_vertices())
overlap = max(min2 - max1, min1 - max2) colliding = all(gap < 0 for gap in gaps)
overlaps.append(overlap)
self.draw_axis(surface, normal, overlap,
min1, max1, min2, max2)
colliding = all(overlap < 0 for overlap in overlaps)
if colliding: if colliding:
text_screen(surface, WHITE, (10, HEIGHT - 20), text_screen(surface, WHITE, (10, HEIGHT - 20),
f"Min Distance to Resolve: {max(overlaps)}") f"Min Distance to Resolve: {max(gaps)}")
self.shape1.draw(surface, YELLOW, 1) self.shape1.draw(surface, YELLOW, 1)
self.shape2.draw(surface, RED if colliding else WHITE, 1) self.shape2.draw(surface, RED if colliding else WHITE, 1)
def draw_axis(self, surface, normal, gap, min1, max1, min2, max2):
"""Draws a separating axis with shape projections, and separating line.
Uses colours to clearly indicate whether the projections overlap."""
# since the normal's origin is 0,0 (center of the screen), calcuate
# an offset to move all drawing off to one side - in this case 300px.
offset = scale(*rnormal(*normal), 300)
overlapping = gap < 0
if not overlapping:
# how far from the center of the gap is from the origin
dist = scale(*normal, (min1 if min1 > min2 else min2) - 0.5 * gap)
# the separating line
line(surface, CYAN,
add(*dist, *offset),
add(*dist, *scale(*lnormal(*normal), 1000)))
# axis, extending off the screen in both directions
line(surface, WHITE,
add(*offset, *scale(*reverse(*normal), 1000)),
add(*offset, *scale(*normal, 1000)))
# projection 1
line(surface, RED if overlapping else GREEN,
add(*offset, *scale(*normal, min1)),
add(*offset, *scale(*normal, max1)), 8)
# projection 2
line(surface, RED if overlapping else GREEN,
add(*offset, *scale(*normal, min2)),
add(*offset, *scale(*normal, max2)), 8)