Starfields and Galaxies with Python
A long time ago i spend a lot of my resources programming fractals and "natural" phenomenons. Since my last adventure with pygame i was playing around with the idea of implementing some algorithms of old times in python anew and use pygame to test them. A small module that creates screens with stars on it is what i will speak about here.
I implemented three small but usefull algorithms:
- A simple function that fills a screen with stars simulating a normal night
- A function that creates clouds of stars similar to eliptic galaxies
- A function that creates spiral galaxies
Although i used pygame for the implementation and although you need it if you want to test my code, it is independend of any (external) library you might use for your game, app, rendering. All you have to do is to overwrite the "draw" method in your own class.
So here is the way to do it:
Pygame Framework to test and Starfields-skeleton
Use the following code below for testing. Just take the comments away from the function call you want to use. As you can see i have put my class in a module called starfields located in a package called natural.
#! /usr/bin/env python import pygame, sys from natural.starfields import Starfields from pygame.color import THECOLORS from math import pi def main(): width=800 height=600 displaymode=(width,height) screen = pygame.display.set_mode(displaymode) pygame.display.set_caption('Starfield') #an array of colors for the stars. probability is determined by the amount of times a color is mentioned colors=[THECOLORS["white"],THECOLORS["yellow"], THECOLORS["white"],THECOLORS["red"],THECOLORS["white"]] s=Starfields(screen,width,height) #s.createRandomStars(100, colors) #s.createStarsByProbability(10, colors) turn = 45.0 * 2 * pi / 360.0 #calculate in rad deg = 270.0 * 2 * pi / 360.0 #s.createElipticStarfield(1000, colors, (300,200), (200,100), turn) s.createSpiralGalaxy(colors,(300,200), (100,50),turn,deg) while True: pygame.display.update() event = pygame.event.poll() if event.type == pygame.QUIT: sys.exit() if __name__=="__main__": main()
Code for the Starfields - class (Skeleton):
#!/usr/bin/env python # -*- coding: utf-8 -*- import random from math import sin, cos,pi try: import pygame except: print "pygame not available, drawing won't work" class Starfields(): def __init__(self,screen, width, height): self.screen=screen self.width=width self.height=height random.seed() """ overwrite this for your own graphics library """ def draw(self,x,y,color): pygame.draw.circle(self.screen,color,(x,y),0)
Initialization needs some "screen" object and the width and height of it. pygame is NOT needed for you LATER. I only included it because i wanted to test the module. You can later adapt rather easily by supplying whatever "surface" you want to draw on and adapting the draw method. For example you might provice a screen of None and in the draw method only assemble an array of data etc.
Doing the starfield
First i used a somewhat simple way to create a starfield. I iterated over the whole screen (pixel by pixel) and rolled some dice checking if there would be a star at that pixel. If so then i would determine which color it had and draw it. The way works, but it costs you a huge amount of time. So instead of doing that you should rather implement a simple algorithm that takes the amount of stars to set and then just determines where to put them and how they should look like.
def createRandomStars(self,amount,colors): lencol=len(colors) for star in range(0,amount): self.draw(random.randint(0,self.width),random.randint(0,self.height),colors[random.randint(0,lencol-1)])
2: Doing the cloud
A cloud is normally deternmined by an elipse with a radius in x direction and one in y, as well as a center. The algorithm is rather simple again. For each star we pick one point at the outer border of the elipse. We first determine the degree randomly. If now we would calculate the positions for x and y position by multilying the radiuses (rx and ry) with the cosinus or sinus then we would create a dotted elipse with the stars all on the edge of it. So we need to move the stars randomly toward the center, with the majority of stars near it and only few at the edge of the elipse. This is done when calculating the insideFactor. It is allways below or equal to 1 and multiplying it with itself will further decrease the value. Around 50 % of all stars should be in the first 25% of the distance between center and outer rim. Finally i also included a variable "turn" which will turn the cloud by a given angle, so that you do not always have the standard "left to right" cloud.
def createElipticStarfield(self, amount, colors, center, radius, turn=0): lencol=len(colors) rx,ry=radius x,y=center for star in range(0,amount): degree=random.randint(0,360) degree= 2.0 * degree * pi / 360.0 #(sin(degree)*rx/ypos=cos(degree)*ry) would form the elipse #since we need to draw inside with the density increaing near the center we must include #some factor (0..1) insideFactor=float(random.randint(0,10000))/10000 insideFactor=insideFactor*insideFactor xpos=sin(degree)*round(insideFactor*rx) ypos=cos(degree)*round(insideFactor*ry) if turn!=0: xptemp=cos(turn)*xpos+sin(turn)*ypos yptemp=-sin(turn)*xpos+cos(turn)*ypos xpos=xptemp ypos=yptemp self.draw(x+xpos,y+ypos,colors[random.randint(0,lencol-1)])
3: Doing the spiral galaxy
Now THIS was rather complicated. The first step is to create ONE single cloud in the center of the spiral galaxy. Then we must create the arms. thats done by putting smaller clouds on the path of those arms. In each iteration we swap between the two spiral arms when putting the clouds. The factors for sizes are actually just the result of several hours of trying.
def createSpiralGalaxy(self, colors, center, size, turn=0, deg=0, dynsizefactor=50, sPCFactor=8): sx,sy=size sx=2.0*sx*pi/360.0 sy=2.0*sy*pi/360.0 x,y=center swap=True xp1=round(deg/pi*sx/1.7)*dynsizefactor yp1=round(deg/pi*sy/1.7)*dynsizefactor #factors for dynamic sizing print xp1,yp1 self.createElipticStarfield(5*(xp1+yp1),colors,center,(xp1,yp1),turn) #this was the central cloud #now for the smaller ones in the spiral arms mulStarAmount=(xp1+yp1)/sPCFactor #factor for amount of stars per cloud n=0.0 while n<=deg: swap = not swap xpos=(cos(n)*((n*sx))*dynsizefactor) ypos=(sin(n)*((n*sy))*dynsizefactor) xp1=cos(turn)*xpos + sin(turn)*ypos yp1=-sin(turn)*xpos + cos(turn)*ypos sizetemp=2+(mulStarAmount*n) if swap: self.createElipticStarfield(int(sizetemp/2),colors,(x+xp1,y+yp1),(sizetemp,sizetemp), turn) else: self.createElipticStarfield(int(sizetemp/2),colors,(x-xp1,y-yp1),(sizetemp,sizetemp), turn) angle=random.randint(0,4)+1 n+= 2.0* angle *pi / 360.0
I would like to have some feedback on this. If you have a good picture send it to me. If you have an idea on how to improove the code or add some functionality to beautify the result ... send it to me.