问题
I am currently trying to replicate in python/pygame (from java, p5 from the coding train video: https://www.youtube.com/watch?v=BZUdGqeOD0w) the water ripple effect.
I am unable to make it work, the screen stays blank (black: background color) or I gives me the error:
Traceback (most recent call last): File "/Users/vicki/Documents/Atom Test/Water Ripple.py", line 39, in <module> screen.fill((current[i][j],current[i][j],current[i][j]),(i,j,1,1)) TypeError: invalid color argument
which I actually find to be normal since we substrate a positive number from 0 to get the color of the pixel:
current[i][j] = (previous[i-1][j]+previous[i+1][j]+ previous[i][j+1]+ previous[i][j-1])/ 2 - current[i][j]
The: previous[i-1][j]
, previous[i+1][j]
, previous[i][j-1]
, previous[i][j+1]
are all equal to 0, whereas the current[I][j]
is a positive number, so logically it should give a negative color value.
Maybe it has to do with how the arrays work in Python vs Java?
For more help, here is the website coding train has used to make his video: https://web.archive.org/web/20160418004149/http://freespace.virgin.net/hugo.elias/graphics/x_water.htm
Here is my code:
import pygame
pygame.init()
width = 200
height = 200
cols = width
rows = height
#dampening = how fast the ripple effect stops.
dampening = 0.999
#Arrays that hold the colors of the screen.
current = [[0]*cols]*rows
previous = [[0]*cols]*rows
screen = pygame.display.set_mode((width,height))
#Sets the initial background to black
screen.fill((0,0,0))
#Mainloop
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
if event.type == pygame.MOUSEBUTTONDOWN:
#if the user clicks the screen, it sets the pixel the user clicked upon white
mouse_pos = pygame.mouse.get_pos()
current[mouse_pos[0]][mouse_pos[1]] = 255
#This part seems to be the problem
for i in range(1,rows-1):
for j in range(1,cols-1):
#This computation is weird: we are substracting positive/zero number from a null number which should normally give us a negative number. A color value (RGB) cannot be a negative number!
current[i][j] = (previous[i-1][j]+previous[i+1][j]+ previous[i][j+1]+ previous[i][j-1])/ 2 - current[i][j]
current[i][j] *= dampening
#This line is where it gives me the error
screen.fill((current[i][j],current[i][j],current[i][j]),(i,j,1,1))
#Switching the arrays, have also tried: previous, current = current, previous (doesn't work either)
temp = previous
previous = current
current = temp
pygame.display.flip()
Here is the code of coding train (in java P5): (Note: he used mousedraft and not mouseTouch (in the video he uses mousetouch) but it isn't important.)
// Daniel Shiffman
// http://codingtra.in
// http://patreon.com/codingtrain
// 2D Water Ripples
// Video: https://youtu.be/BZUdGqeOD0w
// Algorithm: https://web.archive.org/web/20160418004149/http://freespace.virgin.net/hugo.elias/graphics/x_water.htm
int cols;
int rows;
float[][] current;// = new float[cols][rows];
float[][] previous;// = new float[cols][rows];
float dampening = 0.99;
void setup() {
size(600, 400);
cols = width;
rows = height;
current = new float[cols][rows];
previous = new float[cols][rows];
}
void mouseDragged() {
previous[mouseX][mouseY] = 500;
}
void draw() {
background(0);
loadPixels();
for (int i = 1; i < cols-1; i++) {
for (int j = 1; j < rows-1; j++) {
current[i][j] = (
previous[i-1][j] +
previous[i+1][j] +
previous[i][j-1] +
previous[i][j+1]) / 2 -
current[i][j];
current[i][j] = current[i][j] * dampening;
int index = i + j * cols;
pixels[index] = color(current[i][j]);
}
}
updatePixels();
float[][] temp = previous;
previous = current;
current = temp;
}
回答1:
First of all
current = [[0]*cols]*rows previous = [[0]*cols]*rows
is not a 2 dimensional array. It generate a single list and than a outer list is generated where each element refers to the same inner list.
If you want to generate a list, where each element is a separate list, then you have to:
(The inner dimension should be rows e.g. current[col][row]
/current[x][y]
)
current = [[0]*rows for col in range(cols)]
previous = [[0]*rows for col in range(cols)]
In pygame the values for the color channel have to be integral values in range [0, 255]:
val = min(255, max(0, round(current[i][j])))
screen.fill((val, val, val), (i,j,1,1))
Furthermore the MOUSEBUTTONDOWN
event occurs only once when a mouse button is pressed. u have to evaluate if the current state of the mouse is pressed by pygame.mouse.get_pressed():
while True:
# [...]
if any(pygame.mouse.get_pressed()):
mouse_pos = pygame.mouse.get_pos()
previous[mouse_pos[0]][mouse_pos[1]] = 500
I recommend to use pygame.Surface respectively pygame.PixelArray to improve the performance. That's similar loadPixels()/updatePixels() in Processing:
img = pygame.Surface((width, height))
while True:
# [...]
pixelArray = pygame.PixelArray(img)
for i in range(1,cols-1):
for j in range(1,rows-1):
current[i][j] = (
previous[i-1][j] +
previous[i+1][j] +
previous[i][j-1] +
previous[i][j+1]) / 2 - current[i][j]
current[i][j] *= dampening
val = min(255, max(0, round(current[i][j])))
pixelArray[i, j] = (val, val, val)
pixelArray.close()
screen.blit(img, (0, 0))
Sadly that all is still incredibly slow. Full example:
import pygame
pygame.init()
width = 300
height = 200
cols = width
rows = height
#dampening = how fast the ripple effect stops.
dampening = 0.999
#Arrays that hold the colors of the screen.
current = [[0]*rows for col in range(cols)]
previous = [[0]*rows for col in range(cols)]
print(current[0][0], current[199][199])
screen = pygame.display.set_mode((width,height))
#Sets the initial background to black
screen.fill((0,0,0))
img = pygame.Surface((width, height))
#Mainloop
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
if any(pygame.mouse.get_pressed()):
mouse_pos = pygame.mouse.get_pos()
previous[mouse_pos[0]][mouse_pos[1]] = 500
#This part seems to be the problem
pixelArray = pygame.PixelArray(img)
for i in range(1,cols-1):
for j in range(1,rows-1):
current[i][j] = (
previous[i-1][j] +
previous[i+1][j] +
previous[i][j-1] +
previous[i][j+1]) / 2 - current[i][j]
current[i][j] *= dampening
val = min(255, max(0, round(current[i][j])))
pixelArray[i, j] = (val, val, val)
pixelArray.close()
# Switching the arrays
previous, current = current, previous
screen.blit(img, (0, 0))
pygame.display.flip()
For really good performance, you must be using NumPy. In the following example I have throttled the frames per second with pygame.time.Clock.tick:
import numpy
import pygame
import scipy
pygame.init()
window = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()
size = window.get_size()
dampening = 0.999
current = numpy.zeros(size, numpy.float32)
previous = numpy.zeros(size, numpy.float32)
kernel = numpy.array([[0.0, 0.5, 0], [0.5, 0, 0.5], [0, 0.5, 0]])
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if any(pygame.mouse.get_pressed()):
mouse_pos = pygame.mouse.get_pos()
previous[mouse_pos] = 1000
# either:
# current = (scipy.ndimage.convolve(previous, kernel) - current) * dampening
# or:
current[1:size[0]-1, 1:size[1]-1] = (
(previous[0:size[0]-2, 0:size[1]-2] +
previous[2:size[0], 0:size[1]-2] +
previous[0:size[0]-2, 2:size[1]] +
previous[2:size[0], 2:size[1]]) / 2 -
current[1:size[0]-1, 1:size[1]-1]) * dampening
array = numpy.transpose(255 - numpy.around(numpy.clip(current, 0, 255)))
array = numpy.repeat(array.reshape(*size, 1).astype('uint8'), 3, axis = 2)
image = pygame.image.frombuffer(array.flatten(), size, 'RGB')
previous, current = current, previous
window.blit(image, (0, 0))
pygame.display.flip()
pygame.quit()
exit()
来源:https://stackoverflow.com/questions/60336688/water-ripple-effect-python-and-pygame-from-coding-train-video