SAT Algorithm for Collision Detection problem - python

I have a problem with my SAT Algorithm I'm working on. I'm trying to detect a collision between 2 squares (polygons), my algorithm works fine when they are colliding from the top side of one square and bottom side of the other square or vice versa, but when they are on the same y coordinate the algorithm detects collision even if there's a tiny gap between them and they are not colliding. I'm doing this in pygame so I thought there's maybe a problem with its Vector2 library and my algorithm, I don't know. The algorithm iterates through a list of vertices of the 2 squares that I am testing for collision to find all of the edges of both squares. Then I find the normal of each edge and use it as an axis that I'm projecting vertices on. Comparing those projections, I'll find if they overlap or not.
Example of how it looks like
from pygame.locals import *
from pygame import Vector2
from objects import *
pygame.init()
screen = pygame.display.set_mode((500,500))
def distance(a, b):
dx = a.x - b.x
dy = a.y - b.y
return math.sqrt(dx * dx + dy * dy)
def normalize(vector):
invLen = 1 / math.sqrt(vector.x * vector.x + vector.y * vector.y)
return Vector2(vector.x * invLen, vector.y * invLen)
def IntersectPolygons(verticesA, verticesB):
for i in range(len(verticesA)):
v1 = verticesA[i]
v2 = verticesA[(i+1)%len(verticesA)]
#edge of the polygon
edge = v2 - v1
#axis that we will project our vertices on
axis = Vector2(-edge.y, edge.x)
axis = normalize(axis)
[minA, maxA] = ProjectVertices(verticesA, axis)
[minB, maxB] = ProjectVertices(verticesB, axis)
if(minA >= maxB or minB >= maxA):
return False
for i in range(len(verticesB)):
v1 = verticesB[i]
v2 = verticesB[(i+1)%len(verticesB)]
#edge of the polygon
edge = v2 - v1
#axis that we will project our vertices on
axis = Vector2(-edge.y, edge.x)
axis = normalize(axis)
[minA, maxA] = ProjectVertices(verticesA, axis)
[minB, maxB] = ProjectVertices(verticesB, axis)
if(minA >= maxB or minB >= maxA):
return False
return True
def ProjectVertices(vertices, axis):
min = math.inf
max = -math.inf
for v in vertices:
#projection of a vertice onto an axis
proj = Vector2.dot(v, axis)
if proj < min:
min = proj
if proj > max:
max = proj
return [min, max]
sqr1 = Rectangle(250,150,1000,0.2,50)
sqr2 = Rectangle(120,150,1000,0.2,50)
running = True
key = ""
Clock = pygame.time.Clock()
while running:
screen.fill((255,255,255))
sqr1.draw(screen)
sqr2.draw(screen)
vertices1 = sqr1.Vertices()
vertices2 = sqr2.Vertices()
# intersect of two polygons
if IntersectPolygons(vertices1, vertices2):
sqr2.changeColor(col=(0,0,0))
else:
sqr2.changeColor(col=(0,0,255))
if key == "s":
sqr1.y += 1
elif key == "w":
sqr1.y -= 1
if key == "d":
sqr1.x += 1
if key == "a":
sqr1.x -= 1
pygame.display.update()
Clock.tick(60)
for e in pygame.event.get():
if e.type == pygame.QUIT:
pygame.quit()
running = False
if e.type == MOUSEBUTTONDOWN:
print(e.pos)
if e.type == KEYDOWN:
key = e.unicode
if e.type == KEYUP:
key = ""
Before collision
It changes color even If they are not colliding
The algorithm works if I change the axis in the first loop to Vector2(-edge.x, edge.y), but that doesn't make sense since the axis is a normal of the edge but with switched coordinates.

Related

Pygame: How to shoot objects in random directions?

Objects all go off in the same line (45 degrees to the left)...
When I extract the random_direction function to test it by itself, it gives the same vectors just flipped 180 or the x is the same and y is the same but negative... stuff like that.
import pygame
import os
import math
import random
from pygame.math import Vector2
def random_direction():
vector = Vector2(random.uniform(-max_speed, max_speed), random.uniform(-max_speed, max_speed))
if vector.length() == 0:
return vector
else:
return Vector2.normalize(vector)
def scaled(vector, scale):
if vector.length() == 0:
return vector
else:
return Vector2.normalize(vector) * scale
def clamped(vector, limit):
if vector.length() <= limit or vector.length() == 0:
return vector
else:
return Vector2.normalize(vector) * limit
def shoot():
for i in range(len(boids)):
boids[i]['velocity'] = boids[i]['desired_direction'] * max_speed
boids[i]['boid'].x += boids[i]['velocity'].x
boids[i]['boid'].y += boids[i]['velocity'].x
# if boids[i]['boid'].x >= WIDTH:
# boids[i]['boid'].x = 0
# elif boids[i]['boid'].x <= 0:
# boids[i]['boid'].x = WIDTH
# elif boids[i]['boid'].y >= HEIGHT:
# boids[i]['boid'].y = 0
# elif boids[i]['boid'].y <= 0:
# boids[i]['boid'].y = HEIGHT
def draw_window():
WIN.fill((0, 0, 0))
# for i in range(n):
# rot_image = pygame.transform.rotate(image_scaled, math.degrees(math.atan2(boids[i]['velocity'].x, boids[i]['velocity'].y)) +180)
# WIN.blit(rot_image, (boids[i]['boid'].x - int(rot_image.get_width()/2), boids[i]['boid'].y - int(rot_image.get_height()/2))) #########
for i in range(len(boids)):
WIN.blit(image_scaled, (boids[i]['boid'].x, boids[i]['boid'].y))
pygame.display.update()
WIDTH, HEIGHT = 1440, 720 #1680, 990
WIN = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Sim')
FPS = 60
image = pygame.image.load(os.path.join('Assets', 'long_fish.png'))
image_width, image_height = 40, 40
image_scaled = pygame.transform.scale(image, (image_width, image_height))
#boid = pygame.Rect(WIDTH/2, HEIGHT/2, image_width, image_height)
max_speed = 10 #2
steer_strength = 0.04 #2
wander_strength = 0.4 #0.2
# desired_direction = Vector2(0, 0)
# velocity = Vector2(0, 0)
# shoot_direction = random_direction()
n = 30
boids = []
for i in range(n):
boids.append({'boid': pygame.Rect(WIDTH/2, HEIGHT/2, image_width, image_height),
'desired_direction': random_direction(),
'velocity': Vector2(0,0)})
def main():
clock = pygame.time.Clock()
run = True
while run:
clock.tick(FPS)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
shoot()
draw_window()
pygame.quit()
if __name__ == '__main__':
main()
I've made things like this before on pygame and they work but I'm not sure why they do and this doesn't.
Your vector generator generates with bounds of a square:
because of the way you set it up. You then normalize the vector, putting whatever it raised into the bounding circle. Because of this method, values nearer to the corners are more likely to be chosen. From the origin to the edge, there is a distance of one. From the origin to the corner, there is a distance of 1.41 . Thus, values that normalize to a corner are more likely to be chosen
This can give you the impression of not having truly random values, as some values pop up more frequently than others.
The way around this is to generate an already normalized vector, py choosing a point from a circle.
The best way to do this is to generate an angle, in radians. Ex:
>>> angle = math.radians(random.randint(0, 360))
Then, use some basic trigonometry to turn that angle into a point
>>> x = math.cos(angle)
>>> y = math.sin(angle)
The tuple (x, y) should be an unbiased value, that is as random as your pseudorandom (how computers do random, it's actually a big complex equation that generates a value really close to random, but it actually isn't) generator will get.
Implementing this into your code:
def random_direction():
a = math.radians(random.randint(0, 360))
return pygame.Vector2(math.cos(a), math.sin(a))

Problem with animating a sprite in pygame

i have a problem with this code, i am a new person with programming and been using the book "how to think like a computer scientist 3rd edition" and he did not solve exercise 2 of chapter 17 this given: "he deliberately left a mistake in the code to animate Duke. If you click on one of the checkerboard squares to the right of Duke, he salutes anyway. Why? Find a one-line solution to the error ", I've tried many forms but I have not succeeded, I leave you all the code and the images that I have used
PS: images must have the name: ball.png and duke_spritesheet.png
import pygame
gravity = 0.025
my_clock = pygame.time.Clock()
class QueenSprite:
def __init__(self, img, target_posn):
self.image = img
self.target_posn = target_posn
(x, y) = target_posn
self.posn = (x, 0) # Start ball at top of its column
self.y_velocity = 0 # with zero initial velocity
def update(self):
self.y_velocity += gravity
(x, y) = self.posn
new_y_pos = y + self.y_velocity
(target_x, target_y) = self.target_posn # Unpack the position
dist_to_go = target_y - new_y_pos # How far to our floor?
if dist_to_go < 0: # Are we under floor?
self.y_velocity = -0.65 * self.y_velocity # Bounce
new_y_pos = target_y + dist_to_go # Move back above floor
self.posn = (x, new_y_pos) # Set our new position.
def draw(self, target_surface): # Same as before.
target_surface.blit(self.image, self.posn)
def contains_point(self, pt):
""" Return True if my sprite rectangle contains point pt """
(my_x, my_y) = self.posn
my_width = self.image.get_width()
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)
def handle_click(self):
self.y_velocity += -2 # Kick it up
class DukeSprite:
def __init__(self, img, target_posn):
self.image = img
self.posn = target_posn
self.anim_frame_count = 0
self.curr_patch_num = 0
def update(self):
if self.anim_frame_count > 0:
self.anim_frame_count = (self.anim_frame_count + 1 ) % 60
self.curr_patch_num = self.anim_frame_count // 6
def draw(self, target_surface):
patch_rect = (self.curr_patch_num * 50, 0,
50, self.image.get_width())
target_surface.blit(self.image, self.posn, patch_rect)
def contains_point(self, pt):
""" Return True if my sprite rectangle contains pt """
(my_x, my_y) = self.posn
my_width = self.image.get_width()
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)
def handle_click(self):
if self.anim_frame_count == 0:
self.anim_frame_count = 5
def draw_board(the_board):
""" Draw a chess board with queens, as determined by the the_board. """
pygame.init()
colors = [(255,0,0), (0,0,0)] # Set up colors [red, black]
n = len(the_board) # This is an NxN chess board.
surface_sz = 480 # Proposed physical surface size.
sq_sz = surface_sz // n # sq_sz is length of a square.
surface_sz = n * sq_sz # Adjust to exactly fit n squares.
# Create the surface of (width, height), and its window.
surface = pygame.display.set_mode((surface_sz, surface_sz))
ball = pygame.image.load("ball.png")
# Use an extra offset to centre the ball in its square.
# If the square is too small, offset becomes negative,
# but it will still be centered :-)
ball_offset = (sq_sz-ball.get_width()) // 2
all_sprites = [] # Keep a list of all sprites in the game
# Create a sprite object for each queen, and populate our list.
for (col, row) in enumerate(the_board):
a_queen = QueenSprite(ball,
(col*sq_sz+ball_offset, row*sq_sz+ball_offset))
all_sprites.append(a_queen)
# Load the sprite sheet
duke_sprite_sheet = pygame.image.load("duke_spritesheet.png")
# Instantiate two duke instances, put them on the chessboard
duke1 = DukeSprite(duke_sprite_sheet,(sq_sz*2, 0))
duke2 = DukeSprite(duke_sprite_sheet,(sq_sz*5, sq_sz))
# Add them to the list of sprites which our game loop manages
all_sprites.append(duke1)
all_sprites.append(duke2)
while True:
# Look for an event from keyboard, mouse, etc.
ev = pygame.event.poll()
if ev.type == pygame.QUIT:
break;
if ev.type == pygame.KEYDOWN:
key = ev.dict["key"]
if key == 27: # On Escape key ...
break # leave the game loop.
if key == ord("r"):
colors[0] = (255, 0, 0) # Change to red + black.
elif key == ord("g"):
colors[0] = (0, 255, 0) # Change to green + black.
elif key == ord("b"):
colors[0] = (0, 0, 255) # Change to blue + black.
if ev.type == pygame.MOUSEBUTTONDOWN: # Mouse gone down?
posn_of_click = ev.dict["pos"] # Get the coordinates.
for sprite in all_sprites:
if sprite.contains_point(posn_of_click):
sprite.handle_click()
break
for sprite in all_sprites:
sprite.update()
# Draw a fresh background (a blank chess board)
for row in range(n): # Draw each row of the board.
c_indx = row % 2 # Alternate starting color
for col in range(n): # Run through cols drawing squares
the_square = (col*sq_sz, row*sq_sz, sq_sz, sq_sz)
surface.fill(colors[c_indx], the_square)
# Now flip the color index for the next square
c_indx = (c_indx + 1) % 2
# Ask every sprite to draw itself.
for sprite in all_sprites:
sprite.draw(surface)
my_clock.tick(60) # Waste time so that frame rate becomes 60 fps
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
draw_board([0, 5, 3, 1, 6, 4, 2]) # 7 x 7 to test window size
PS: I think the error is here but it did not succeed
return ( x >= my_x and x + my_width and y >= my_y and y < my_y + my_height)
The issue is caused by the face, that "duke_spritesheet.png" is a sprite sheet. When you define the rectangular region which is covered by the object, then you have to use the width of a single image, rather than the width of the entire sprite sheet:
my_width = self.image.get_width()
my_width = 50
Change this in the method contains_point of the class DukeSprite:
class DukeSprite:
# [...]
def contains_point(self, pt):
""" Return True if my sprite rectangle contains pt """
(my_x, my_y) = self.posn
my_width = 50
my_height = self.image.get_height()
(x, y) = pt
return ( x >= my_x and x < my_x + my_width and
y >= my_y and y < my_y + my_height)

Close range 3d display messed up

I copied a code of YouTube, about displaying 3d cubes on a screen in Python, without the use of external modules (like PyOpenGL). It works fine, but the moment you go between two cubes, the display gets messed up. Here is my code:
import pygame, sys, math, random
def rotate2d(pos, rad): x,y=pos; s,c = math.sin(rad),math.cos(rad); return x*c-y*s,y*c+x*s
class Cam:
def __init__(self, pos=(0,0,0),rot=(0,0)):
self.pos = list(pos)
self.rot = list(rot)
def events(self, event):
if event.type == pygame.MOUSEMOTION:
x, y = event.rel; x/=200; y/=200
self.rot[0]+=y; self.rot[1]+=x
def update(self, dt, key):
s = dt*10
if key[pygame.K_q]: self.pos[1]+=s
if key[pygame.K_e]: self.pos[1]-=s
x,y = s*math.sin(self.rot[1]),s*math.cos(self.rot[1])
if key[pygame.K_w]: self.pos[0]+=x; self.pos[2]+=y
if key[pygame.K_s]: self.pos[0]-=x; self.pos[2]-=y
if key[pygame.K_a]: self.pos[0]-=y; self.pos[2]+=x
if key[pygame.K_d]: self.pos[0]+=y; self.pos[2]-=x
if key[pygame.K_r]: self.pos[0]=0; self.pos[1]=0;\
self.pos[2]=-5; self.rot[0]=0; self.rot[1]=0
class Cube:
faces = (0,1,2,3),(4,5,6,7),(0,1,5,4),(2,3,7,6),(0,3,7,4),(1,2,6,5)
colors = (255,0,0),(255,128,0),(255,255,0),(255,255,255),(0,0,255),(0,255,0)
def __init__(self,pos=(0,0,0),color=None,v0=(-1,-1,-1),v1=(1,-1,-1),v2=(1,1,-1),v3=(-1,1,-1),v4=(-1,-1,1),v5=(1,-1,1),v6=(1,1,1),v7=(-1,1,1)):
if color != None:
if len(color) == 3 or len(color) == 4:
self.colors = tuple((color for i in range(6)))
if len(color) == 6:
self.colors = color
self.vertices = (v0,v1,v2,v3,v4,v5,v6,v7)
x,y,z = pos
self.verts = [(x+X/2,y+Y/2,z+Z/2) for X,Y,Z in self.vertices]
pygame.init()
w,h = 400,400; cx,cy = w//2, h//2
screen = pygame.display.set_mode((w,h))
clock = pygame.time.Clock()
cam = Cam((0,0,-5))
pygame.event.get(); pygame.mouse.get_rel()
pygame.mouse.set_visible(0); pygame.event.set_grab(1)
occupied = []
cube1 = Cube((0,0,0))
cube2 = Cube((0,0,2))
objects = [cube1, cube2]
while True:
dt = clock.tick()/1000
for event in pygame.event.get():
if event.type == pygame.QUIT: pygame.quit(); sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: pygame.quit(); sys.exit()
cam.events(event)
screen.fill((0,0,0))
face_list = []; face_color = []; depth = []
for obj in objects:
vert_list = []; screen_coords = []
for x,y,z in obj.verts:
x-= cam.pos[0]; y-=cam.pos[1];z-=cam.pos[2]
x,z = rotate2d((x,z),cam.rot[1])
y,z = rotate2d((y,z),cam.rot[0])
vert_list += [(x,y,z)]
f = 200/z
x,y = x*f,y*f
screen_coords+=[(cx+int(x),cy+int(y))]
for f in range(len(obj.faces)):
face = obj.faces[f]
on_screen = False
for i in face:
x,y = screen_coords[i]
if vert_list[i][2]>0 and x>0 and x<w and y>0 and y<h: on_screen = True; break
if on_screen:
coords = [screen_coords[i] for i in face]
face_list += [coords]
face_color += [obj.colors[f]]
depth += [sum(sum(vert_list[j][i]**2 for i in range(3)) for j in face) / len(face)]
order = sorted(range(len(face_list)),key=lambda i:depth[i],reverse=1)
for i in order:
try: pygame.draw.polygon(screen,face_color[i],face_list[i])
except: pass
key = pygame.key.get_pressed()
pygame.display.flip()
cam.update(dt,key)
And here is what happens when you try and navigate between the two cubes:
Can someone please suggest an edit to the code that would stop the cubes from becoming distorted when the cam.pos is close to the cubes?
The application does not correctly draw the geometry, when apart of a faces (primitive, side of a cube) is behind and the other part in front of the eye position. That happens if the transformed z coordinate (vert_list += [(x,y,z)]) is positive for the some vertices and negative for negative for some other vertices that form primitive (face).
You can test that behavior with ease, if you skip all the faces, where at least one z coordinate is negative (behind the eye):
while True:
# [...]
for obj in objects:
# [...]
for f in range(len(obj.faces)):
face = obj.faces[f]
#on_screen = False
#for i in face:
# x,y = screen_coords[i]
# if vert_list[i][2]>0 and x>0 and x<w and y>0 and y<h: on_screen = True; break
# draw a face if any projected coordinate (x, y) is in the viewing volume
on_screen = False
for i in face:
x,y = screen_coords[i]
if x>0 and x<w and y>0 and y<h: on_screen = True; break
# skip a face if NOT ALL z coordinates are positive
if on_screen:
on_screen = all([vert_list[i][2]>0 for i in face])
if on_screen:
# [...]
The issue can be solved by clipping the geometry at hypothetical near plane. See Viewing frustum:
while True:
# [...]
for obj in objects:
# [...]
for f in range(len(obj.faces)):
face = obj.faces[f]
on_screen = False
for i in face:
x,y = screen_coords[i]
if vert_list[i][2]>0 and x>0 and x<w and y>0 and y<h: on_screen = True; break
# clip geometry at near plane
if on_screen:
near = 0.01
for i in face:
if vert_list[i][2]<0:
x, y, z = vert_list[i]
nearscale = 200/near
x,y = x*nearscale,y*nearscale
screen_coords[i] = (cx+int(x),cy+int(y))
if on_screen:
coords = [screen_coords[i] for i in face]
face_list += [coords]
face_color += [obj.colors[f]]
depth += [sum(sum(vert_list[j][i]**2 for i in range(3)) for j in face) / len(face)]

Make cursor unable to move through sprite pygame

So my question is simple: How do I make a sprite that my mouse can't pass through? I've been experimenting, and I found an unreliable way to do it that is also super glitchy. If anyone knows how I might go about this, please help.
Here is the code that I am currently using:
import pygame
import pyautogui
import sys
import time
pygame.init()
game_display = pygame.display.set_mode((800,600))
pygame.mouse.set_visible(True)
pygame.event.set_grab(True)
exit = False
class Wall(pygame.sprite.Sprite):
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((30, 100))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
self.rect.center = (200, 200)
def collision(self):
loc = pygame.mouse.get_pos()
yy = loc[1]
xx = loc[0]
if yy >= self.rect.top and yy <= self.rect.bottom and xx >= self.rect.left and xx <= self.rect.right:
if xx >= 200:
pyautogui.move(216 - xx, 0)
if xx <= 200:
pyautogui.move(-xx + 184, 0)
w = Wall()
all_sprites = pygame.sprite.Group()
all_sprites.add(w)
print(w.rect.top)
print(w.rect.bottom)
while (not exit):
mouse_move = (0,0)
for event in pygame.event.get():
if event.type == pygame.QUIT:
exit = True
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
exit = True
w.collision()
clock = pygame.time.Clock()
game_display.fill((0, 0, 0))
clock.tick(30)
all_sprites.update()
all_sprites.draw(game_display)
pygame.display.flip()
pygame.quit()
note: please ignore my extra import statements, I am going to use them for later.
To do what you want you have to check if the line form the previous mouse position to the new mouse position intersects the rectangle. Write a function IntersectLineRec which checks for the intersection and use it and returns a list of intersection points, sorted by the distance.
The function returns a list of tules whith points and distances:
e.g.
[((215.0, 177.0), 12.0), ((185.0, 177.0), 42.0)]
prev_loc = pygame.mouse.get_pos()
class Wall(pygame.sprite.Sprite):
# [...]
def collision(self):
global prev_loc
loc = pygame.mouse.get_pos()
intersect = IntersectLineRec(prev_loc, loc, self.rect)
prev_loc = loc
if intersect:
ip = [*intersect[0][0]]
for i in range(2):
tp = self.rect.center[i] if ip[i] == loc[i] else loc[i]
ip[i] += -3 if ip[i] < tp else 3
pyautogui.move(ip[0]-loc[0], ip[1]-loc[1])
prev_loc = loc = ip
The function IntersectLineRec has to check if one of the 4 outer lines between the 4 corners of the rectangle inter sects the line between the mouse positions:
def IntersectLineRec(p1, p2, rect):
iL = [
IntersectLineLine(p1, p2, rect.bottomleft, rect.bottomright),
IntersectLineLine(p1, p2, rect.bottomright, rect.topright),
IntersectLineLine(p1, p2, rect.topright, rect.topleft),
IntersectLineLine(p1, p2, rect.topleft, rect.bottomleft) ]
iDist = [(i[1], pygame.math.Vector2(i[1][0] - p1[0], i[1][1] - p1[1]).length()) for i in iL if i[0]]
iDist.sort(key=lambda t: t[1])
return iDist
IntersectLineRec checks if to endless lines, which are defined by to points are intersecting. Then it checks if the intersection point is in the rectangles which are defined by the each of the lines (the line is the diagonal of the rectangle):
def IntersectLineLine(l1_p1, l1_p2, l2_p1, l2_p2):
isect, xPt = IntersectEndlessLineLine(l1_p1, l1_p2, l2_p1, l2_p2)
isect = isect and PtInRect(xPt, l1_p1, l1_p2) and PtInRect(xPt, l2_p1, l2_p2)
return isect, xPt
To check if a point is in an axis aligned rectangle has to check if both coordinates of the point are in the range of the coordinates of the rectangle:
def InRange(coord, range_s, range_e):
if range_s < range_e:
return coord >= range_s and coord <= range_e
return coord >= range_e and coord <= range_s
def PtInRect(pt, lp1, lp2):
return InRange(pt[0], lp1[0], lp2[0]) and InRange(pt[1], lp1[1], lp2[1])
The intersection of to endless lines can be calculated like this:
def IntersectEndlessLineLine(l1_p1, l1_p2, l2_p1, l2_p2):
# calculate the line vectors and test if both lengths are > 0
P = pygame.math.Vector2(*l1_p1)
Q = pygame.math.Vector2(*l2_p1)
line1 = pygame.math.Vector2(*l1_p2) - P
line2 = pygame.math.Vector2(*l2_p2) - Q
if line1.length() == 0 or line2.length() == 0:
return (False, (0, 0))
# check if the lines are not parallel
R, S = (line1.normalize(), line2.normalize())
dot_R_nvS = R.dot(pygame.math.Vector2(S[1], -S[0]))
if abs(dot_R_nvS) < 0.001:
return (False, (0, 0))
# calculate the intersection point of the lines
# t = dot(Q-P, (S.y, -S.x)) / dot(R, (S.y, -S.x))
# X = P + R * t
ptVec = Q-P
t = ptVec.dot(pygame.math.Vector2(S[1], -S[0])) / dot_R_nvS
xPt = P + R * t
return (True, (xPt[0], xPt[1]))
See the animation:

Mandelbrot set displays incorrectly

This is my attempt to program the Mandelbrot set in Python 3.5 using the Pygame module.
import math, pygame
pygame.init()
def mapMandelbrot(c,r,dim,xRange,yRange):
x = (dim-c)/dim
y = (dim-r)/dim
#print([x,y])
x = x*(xRange[1]-xRange[0])
y = y*(yRange[1]-yRange[0])
x = xRange[0] + x
y = yRange[0] + y
return [x,y]
def checkDrawBox(surface):
for i in pygame.event.get():
if i.type == pygame.QUIT:
pygame.quit()
elif i.type == pygame.MOUSEBUTTONDOWN:
startXY = pygame.mouse.get_pos()
boxExit = False
while boxExit == False:
for event in pygame.event.get():
if event.type == pygame.MOUSEBUTTONUP:
boxExit = True
if boxExit == True:
return [startXY,pygame.mouse.get_pos()]
pygame.draw.rect(surface,[255,0,0],[startXY,[pygame.mouse.get_pos()[0]-startXY[0],pygame.mouse.get_pos()[1]-startXY[1]]],1)
pygame.display.update()
def setup():
dimensions = 500
white = [255,255,255]
black = [0,0,0]
checkIterations = 100
canvas = pygame.display.set_mode([dimensions,dimensions])
canvas.fill(black)
xRange = [-2,2]
yRange = [-2,2]
xRangePrev = [0,0]
yRangePrev = [0,0]
newxRange = [0,0]
newyRange = [0,0]
while True:
if not ([xRange,yRange] == [xRangePrev,yRangePrev]):
draw(dimensions, canvas, xRange, yRange, checkIterations)
pygame.display.update()
xRangePrev = xRange
yRangePrev = yRange
box = checkDrawBox(canvas)
if box != None:
maxX = max([box[0][0],box[1][0]])
maxY = max([box[0][1],box[1][1]])
newxRange[0] = mapMandelbrot(box[0][0],0,dimensions,xRange,yRange)[0]
newxRange[1] = mapMandelbrot(box[1][0],0,dimensions,xRange,yRange)[0]
newyRange[0] = mapMandelbrot(0,box[0][1],dimensions,xRange,yRange)[1]
newyRange[1] = mapMandelbrot(0,box[1][1],dimensions,xRange,yRange)[1]
xRange = newxRange
yRange = newyRange
def draw(dim, surface, xRange, yRange, checkIterations):
for column in range(dim):
for row in range(dim):
greyVal = iteration(0,0,mapMandelbrot(column,row,dim,xRange,yRange),checkIterations,checkIterations)
surface.set_at([dim-column,row],greyVal)
def iteration(a, b, c, iterCount, maxIter):
a = (a*a) - (b*b) + c[0]
b = (2*a*b) + c[1]
iterCount = iterCount - 1
if iterCount == 0:
return [0,0,0]
elif abs(a+b) > 17:
b = (iterCount/maxIter)*255
return [b,b,b]
else:
return iteration(a,b,c,iterCount,maxIter)
setup()
I believe that the iteration algorithm is correct, but the output doesn't look right:
Wondering what might be the problem? Sorry for the code dump, just not sure which part may cause it to look like that.
Fascinating bug -- it literally looks like a squashed bug :)
The problem lies in the two lines:
a = (a*a) - (b*b) + c[0]
b = (2*a*b) + c[1]
You are changing the meaning of a in the first line, hence using the wrong a in the second.
The fix is as simple as:
a, b = (a*a) - (b*b) + c[0], (2*a*b) + c[1]
which will cause the same value of a to be used in calculating the right hand side.
It would be interesting to work out just what your bug has produced. Even though it isn't the Mandelbrot set, it seems to be an interesting fractal in its own right. In that sense, you had a very lucky bug. 99% percent of the times, bugs lead to garbage, but every now and then they produce something quite interesting, but simply unintended.
On Edit:
The Mandelbrot set is based on iterating the complex polynomial:
f(z) = z^2 + c
The pseudo-Mandelbrot set which this bug has produced is based on iterating the function
f(z) = Re(z^2 + c) + i*[2*Re(z^2 + c)*Im(z) + Im(c)]
where Re() and Im() are the operators which extract the real and imaginary parts of a complex number. This isn't a polynomial in z, though it is easy to see that it is a polynomial in z,z* (where z* is the complex conjugate of z). Since it is a fairly natural bug, it is almost certain that this has appeared somewhere in the literature on the Mandelbrot set, though I don't remember ever seeing it.
I decided to learn about the mandelbrot set and wrote my own version! I used python's complex data type, which should make the mandelbrot calculation for each pixel a bit clearer. Here is a screenshot of the result:
And here is the source code/code dump:
import pygame
import sys
def calc_complex_coord(x, y):
real = min_corner.real + x * (max_corner.real - min_corner.real) / (width - 1.0)
imag = min_corner.imag + y * (max_corner.imag - min_corner.imag) / (height - 1.0)
return complex(real, imag)
def calc_mandelbrot(c):
z = c
for i in range(1, max_iterations+1):
if abs(z) > 2:
return i
z = z*z + c
return i
def calc_color_score(i):
if i == max_iterations:
return black
frac = 255.0 - (255.0 * i / max_iterations)
return (frac, frac, frac)
def update_mandelbrot():
for y in range(height):
for x in range(width):
c = calc_complex_coord(x, y)
mandel_score = calc_mandelbrot(c)
color = calc_color_score(mandel_score)
mandel_surface.set_at((x, y), color)
if __name__ == "__main__":
pygame.init()
(width, height) = (500, 500)
display = pygame.display.set_mode((width, height))
pygame.display.set_caption("Mandelbrot Magic")
clock = pygame.time.Clock()
mandel_surface = pygame.Surface((width, height))
black = (0, 0, 0)
red = (255, 0, 0)
max_iterations = 50
min_corner = complex(-2, -2)
max_corner = complex(2, 2)
box = pygame.Rect(0, 0, width, height)
update_mandel = True
draw_box = False
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.MOUSEBUTTONDOWN:
x, y = event.pos
box = pygame.Rect(x, y, 0, 0)
draw_box = True
elif event.type == pygame.MOUSEMOTION:
x, y = event.pos
if draw_box:
box = pygame.Rect(box.left, box.top, x - box.left, y - box.top)
elif event.type == pygame.MOUSEBUTTONUP:
x, y = event.pos
update_mandel = True
display.blit(mandel_surface, (0, 0))
if draw_box:
pygame.draw.rect(display, red, box, 1)
if update_mandel:
box.normalize()
new_min_corner = calc_complex_coord(box.left, box.top)
new_max_corner = calc_complex_coord(box.right, box.bottom)
min_corner, max_corner = new_min_corner, new_max_corner
update_mandelbrot()
update_mandel = False
draw_box = False
pygame.display.update()
clock.tick(60)
The two issues with this code are that one, it is quite slow in updating the mandelbrot set, and two, the aspect ratios get distorted if you work with non-square windows or box-selections. Let me know if any of the code is unclear!

Categories

Resources