/
metaballs_numpy.py
135 lines (103 loc) · 3.69 KB
/
metaballs_numpy.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
""" Pygame application which calculate metaballs with numpy
"""
import sys
import random
import pygame
import numpy as np
from pygame.locals import HWSURFACE, DOUBLEBUF
from pygame.math import Vector2
from ball import Ball
# config window Size
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
BALL_RADIUS = 5000
N_BALLS = 6
def euclid_dist(vector_p1, vector_p2):
""" calculated the euclidean distance between 2 points """
distances = vector_p1 - vector_p2
return np.hypot(distances[:, :, 0], distances[:, :, 1])
def gray(image, mode="saturate"):
""" create r,g,b layers to create grayscale image from 2d array """
if mode == "saturate":
image[image >= 255] = 255
else:
# Mode normalize
image = 255 * (image / image.max())
width, height = image.shape
ret = np.empty((width, height, 3), dtype=np.uint8)
ret[:, :, 2] = ret[:, :, 1] = ret[:, :, 0] = image
return ret
class MetaBalls:
""" Metaball game """
def __init__(self) -> None:
"""Init pygame and related game objects"""
pygame.init()
self._screen = pygame.display.set_mode(
[SCREEN_WIDTH, SCREEN_HEIGHT], HWSURFACE | DOUBLEBUF
)
self._screen.fill(pygame.Color("black"))
pygame.display.set_caption("MetaBalls")
self._clock = pygame.time.Clock()
self._index_pixel = Vector2(0, 0)
# Init metaballs in random positions
self._balls = []
for _ in range(N_BALLS):
ball = Ball(
self._screen,
Vector2(
random.random() * (SCREEN_WIDTH - 1),
random.random() * (SCREEN_HEIGHT - 1),
),
radius=BALL_RADIUS,
)
self._balls.append(ball)
self._dt_seconds = None
self._grid_xy = np.zeros((SCREEN_WIDTH * SCREEN_HEIGHT * 2)).reshape(
SCREEN_WIDTH, SCREEN_HEIGHT, 2
)
self._img = np.zeros(SCREEN_WIDTH * SCREEN_HEIGHT).reshape(
SCREEN_WIDTH, SCREEN_HEIGHT
)
for pos_x in range(SCREEN_WIDTH):
for pos_y in range(SCREEN_HEIGHT):
self._grid_xy[pos_x][pos_y] = pos_x, pos_y
def show_fps(self) -> None:
""" Show framerate in upper left corner """
font = pygame.font.SysFont("Arial", 18)
fps = str(f"{1/self._dt_seconds:.3f}")
fps_text = font.render(fps, 1, pygame.Color("coral"))
self._screen.blit(fps_text, (10, 0))
def check_events(self) -> None:
""" Check pygame events """
# loop through all events
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
def draw(self) -> None:
""" Draw metaballs to screen """
self._img.fill(0)
for ball in self._balls:
distances = euclid_dist(self._grid_xy, ball.pos_np)
self._img += ball.radius / distances
pygame.surfarray.blit_array(self._screen, gray(self._img))
def update(self) -> None:
""" Update objects """
# Listcomprehension to be fast as possible
[ball.update(self._dt_seconds) for ball in self._balls]
def run(self) -> None:
""" Main game loop to update and draw objects """
# control the draw update speed
self._dt_seconds = self._clock.tick(120) / 1000
# check events
self.check_events()
self.update()
self.draw()
# update the screen with what we've drawn
self.show_fps()
pygame.display.flip()
if __name__ == "__main__":
# initialize MetaBalls pygame
meta_balls = MetaBalls()
while True:
# Run until user exits pygame window
meta_balls.run()