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:

  1. A simple function that fills a screen with stars simulating a normal night
  2. A function that creates clouds of stars similar to eliptic galaxies
  3. 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:

  1. 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.

  2. 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

Finally:

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.