This gets 20fps at 320x240 with one wave on my 700MHz laptop. #!/usr/bin/python
# Simple wave mechanics in PyGame, by Kragen Javier Sitaker, 2007-12-07. # Needs Python, SDL, PyGame, and Numeric Python installed. # Notes on speed: # Sadly at first I was only able to render <17fps with a single wave, # which means it's doing under 1.3 million pixels per second on my # 700MHz PIII-Coppermine, and 11fps with two waves. I thought it was # absurd that it takes more than 500 clock cycles per pixel, # especially given that it's not doing any significant amount of # Python (it's doing about 80 Python bytecodes in the redraw function, # which adds up to 960 pixels per Python bytecode) but I wrote a C # version (just doing all the math inside the loop, instead of in big # arrays) and it was only 50% faster. After some experimentation, I # switched the C version to avoid floating-point math in the inner # loop, to approximate the square root with linear interpolation, and # to replace the sine function and scaling to palette index operations # with a table lookup, and quadrupled its speed, making it about six # times as fast as this Python version at the time. I tried the same # optimizations on this program, and it got slower. # I finally got to 20fps (with one wave) (19% of the C version's # speed) by switching to single-precision float math. The tricky # parts were that single-precision isn't precise enough to express the # current time (so you have to take it mod 2*pi) and that you have to # manually convert each scalar to a single-precision float. import pygame, sys, Numeric, time, math twopi = 2 * math.pi def grayscale_for_masks(masks, level): "Compute a grayscale pixel from bit masks and a floating-point level [0,1)" return sum([int(mask * level) & mask for mask in masks]) class World: "The stuff that gets drawn on the screen." def __init__(self, screen): self.screen = screen width, height = self.screen.get_size() # This is a bit hard to explain, but this makes arrays 'xs' # and 'ys' that contain the x and y coordinates of each pixel. # So every row of the 'xs' array is [0, 1, 2, 3...], and row 0 # of the 'ys' array is [0, 0, 0, 0...], while row 1 is [1, 1, # 1, 1...]. This is somewhat confused by the default Python # display of these guys being transposed if you print them out. (self.xs, self.ys) = (xs, ys) = Numeric.indices((width, height)) # Now we want an array of radii (hi Andy). So we from_center_x = xs - width / 2 from_center_y = ys - height / 2 self.r = (Numeric.sqrt(from_center_x ** 2 + from_center_y ** 2) / (width/64)).astype(Numeric.Float32) self.tmp = self.r.copy() # temp space for later (to reduce per-frame allocation) masks = self.screen.get_masks()[0:3] # Lookup table for grayscale levels. self.palette = Numeric.array([grayscale_for_masks(masks, level/256.0) for level in range(256)]) def add_second_wave(self, to_what): pass def peak(self): return 1.01 # was getting occasional overflow errors on y1 def redraw(self): # This function is written in a fairly assembly-language style # in order to cut down on the number of intermediate result # spaces that must be allocated. tmp = self.tmp # to make code briefer N = Numeric f32 = lambda x: N.array(x, N.Float32) # tmp gets -time.time() + self.r N.add(f32(-time.time() % twopi), self.r, tmp) # tmp gets sin(tmp), i.e. sin(r - time) N.sin(tmp, tmp) self.add_second_wave(tmp) # add a second wave in the subclass # tmp gets tmp + peak, i.e. peak + sin(r - time) N.add(tmp, f32(self.peak()), tmp) # tmp gets tmp * (256/ (2*peak)), i.e. (1 + sin(r-time))/2 * 256 N.multiply(tmp, f32(256 / (self.peak()*2)), tmp) # round floats to Int8 so we can look things up in palette ints = tmp.astype(N.Int8) # Look up the pixel value for each grayscale level in the palette grayscale = N.take(self.palette, ints) # I tried using surfarray.pixels2d and blitting from there, # but that made things like 10% slower. So here we blit_array # onto the screen. pygame.surfarray.blit_array(self.screen, grayscale) class World2Waves(World): def __init__(self, screen): World.__init__(self, screen) # Center our second set of waves at the upper left-hand corner # of the screen instead of the middle, and give it twice as # long a wavelength self.r2 = (Numeric.sqrt(self.xs ** 2 + self.ys ** 2) / (screen.get_width()/32)).astype(Numeric.Float32) self.tmp2 = self.r.copy() def add_second_wave(self, to_what): # our second wave travels slower by a factor of e Numeric.add(Numeric.array(-time.time() / Numeric.e % twopi, Numeric.Float32), self.r2, self.tmp2) Numeric.sin(self.tmp2, self.tmp2) Numeric.add(self.tmp2, to_what, to_what) def peak(self): return 2 def main(argv): pygame.init() screen = pygame.display.set_mode((320, 240), pygame.FULLSCREEN) world = World2Waves(screen) # alternatively just World(screen) frames = 0 start = time.time() while 1: ev = pygame.event.poll() if ev.type == pygame.NOEVENT: frames += 1 world.redraw() pygame.display.flip() elif ev.type == pygame.MOUSEBUTTONDOWN: break elif ev.type == pygame.QUIT: break end = time.time() print "%.2f seconds, %.2f fps" % ((end - start), frames / (end - start)) if __name__ == '__main__': main(sys.argv)