Starfields

Starfields


Starfields are about as old as the hills. The very first demos all had scrolling text against a background of zooming stars. Games have had them, most graphics programmers have made one at some time. Despite this, I have seen some very bad examples indeed. I have also seen some very good ones.

Starfields give the impression that you're flying through space, with stars whizzing by. Though, obviously, you'd have to be moving very fast indeed if you were to do this in real life. There is more than one way to achieve this, some ways being better than others. I will explain the various methods, how good they are, and where you can find examples of them.


A horrendous method
If you have ever played the game Supremacy on the PC, you will have seen just about the worst starfield there is. The rest of the games is very good, so I have no idea how they managed to produce a starfield that gives absolutely no impression of movement whatsoever. Stars appear at the center of the screen, and travel in towards the edge. Many of them travel exactly horizontally or vertically. Massively unimpressive.


variables
 Center_of_screen_X = 160
 Center_of_screen_Y = 100

 array of integers: star_x(0 to 15)  = Center_of_screen_X
 array of integers: star_y(0 to 15)  = Center_of_screen_Y
 array of integers: star_vx(0 to 15) = random numbers between -2 and 2
 array of integers: star_vy(0 to 15) = random numbers between -2 and 2


program
 loop forever

     loop i from 0 to 15

	(erase the old position of the star)
	 draw a black pixel at (star_x(i), star_y(i))

         star_x(i) = star_x(i) + star_vx(i)
         star_y(i) = star_y(i) + star_vy(i)

	 if (star_x(i), star_y(i)) is off then screen then
	     star_x(i)  = Center_of_screen_X
	     star_y(i)  = Center_of_screen_Y
             star_vx(i) = a random number between -2 and 2
             star_vy(i) = a random number between -2 and 2
         end if

	(draw in the star at the new position)
	 draw a white pixel at (star_x(i), star_y(i))

    end of i loop

 end loop


Improved motion
What was missing there was a sense that the stars were coming closer to you. There is no illusion of three dimensionality, the stars look as is they are simply running along the screen. The previous method must be abandoned altogether.
Now, we shall imagine the stars to be points in 3D space, all of them moving towards the viewer, along the Z-axis. At each time step, the 3D coordinates of the stars will be projected onto the screen, and displayed.


variables

 (coordinates of stars in 3D)
  array of floats: star_x(0 to 63)
  array of floats: star_y(0 to 63)
  array of floats: star_z(0 to 63)

 (location of stars on the screen)
  array of integers: star_screenx(0 to 63)
  array of integers: star_screeny(0 to 63)

  Center_of_screen_X = 160
  Center_of_screen_Y = 100

program

 (initialise positions of stars)
  loop i from 0 to 63
      star_x(i) = random number between -500 and  500;
      star_y(i) = random number between -500 and  500;
      star_z(i) = random number between  100 and 1000;
  end of i loop


 (now draw the starfield)
  loop forever

      loop i from 0 to 63

	 (erase the old position of the star)
 	  draw a black pixel at (star_screenx(i), star_screeny(i))

 	  (move the star closer)
	  star_z(i) = star_z(i) - 5;

	  (calculate screen coordinates)
 	  star_screenx(i) = star_x(i) / star_z(i) * 100 + Center_of_screen_X
	  star_screeny(i) = star_y(i) / star_z(i) * 100 + Center_of_screen_Y

	  
	  if (star_screenx(i), star_screeny(i)) is off then screen, 
	     or star_z(i) < 1 then

              star_x(i) = random number between -500 and  500;
              star_y(i) = random number between -500 and  500;
              star_z(i) = random number between  100 and 1000;

	  end if

	 (draw in the star at the new position)
	  draw a white pixel at (star_screenx(i), star_screeny(i))

      end of i loop

  end of loop


Fading in
That was definately an improvement. However, it's not terribly realistic to have stars simply appearing in the distance and flying towards you. For a smoother effect, we can make the stars black when they first appear (so you don't notice them) then get brighter as they get closer.

Assuming you can display 256 shades of grey, change the line

	  draw a white pixel at (star_screenx(i), star_screeny(i))
to
	  b = 255-(starz[i]*(255./1000.))
	  draw a pixel at (star_screenx(i), star_screeny(i)) with brightness b


A sense of scale
Space is big. But the starfield gives little sense of scale. There are two ways the sense of vastness can be modeled. The first is simply to model a huge area of space, which is impractical to say the least. The second is to make the stars move with a range of velocities. You can get away with this because the stars have no apparent size, so slow moving stars will simply appear to be very far away.
In addition, the slower stars will be drawn dimmer to make them look more distant.


variables

 (coordinates of stars in 3D)
  array of floats: star_x(0 to 63)
  array of floats: star_y(0 to 63)
  array of floats: star_z(0 to 63)

 (location of stars on the screen)
  array of integers: star_screenx(0 to 63)
  array of integers: star_screeny(0 to 63)

 (velocity of stars)
  array of floats: star_zv(0 to 63)

  Center_of_screen_X = 160
  Center_of_screen_Y = 100

program

 (initialise positions of stars)
  loop i from 0 to 63
      star_x(i) = random number between -1000 and 1000;
      star_y(i) = random number between -1000 and 1000;
      star_z(i) = random number between  100 and 1000;
      star_zv(i)= random number between  .5 and 5;
  end of i loop


 (now draw the starfield)
  loop forever

      loop i from 0 to 63

	 (erase the old position of the star)
 	  draw a black pixel at (star_screenx(i), star_screeny(i))

 	  (move the star closer)
	  star_z(i) = star_z(i) - star_zv(i);

	  (calculate screen coordinates)
 	  star_screenx(i) = star_x(i) / star_z(i) * 100 + Center_of_screen_X
	  star_screeny(i) = star_y(i) / star_z(i) * 100 + Center_of_screen_Y

	  
	  if (star_screenx(i), star_screeny(i)) is off screen
	     or star_z(i) < 1 then

              star_x(i) = random number between -500 and  500;
              star_y(i) = random number between -500 and  500;
              star_z(i) = random number between  100 and 1000;
              star_zv(i)= random number between  .5 and 5;

	  end if

	 (draw in the star at the new position)
	  b = ( (255/5) * star_zv(i)) * (1000 / starz[i])
	  draw a pixel at (star_screenx(i), star_screeny(i)) with brightness b

      end of i loop

  end of loop


Smoother motion
Now, if you watch the stars carefully as they travel out from the center of the screen you will see them wobble as they jump discretely from one pixel to the next. It would be much nicer if they could move smoothly from one to the next. One way to overcome this is to use a higher resolution, but, even then the effect will be noticable. A good way to smooth out the motion is to use anti-aliased particles. See the article on wu-pixels.


Motion blur
An even better way to smooth out the motion is to use motion blur. Imagine the stars are being filmed by a real camera. The shutter is open for some amount of time, say 1/25th of a second. In that time it is recieving light from the scene it is facing. If anything moves in that time, it's image on the camera film will be blurred. So the moving stars should produce little streaks as they fly past the camera. The faster a star is moving, the darker that streak will be.

Two new arrays will need to be created to hold the old positions of the stars:

.
.
[snip]
.
.

(old positions of the stars on the screen)
  array of floats: star_Oldscreenx(0 to 63)
  array of floats: star_Oldscreeny(0 to 63)

.
.
[snip]
.
.

And this time lines will be drawn from the old to the new position:



         (erase old star)

	  Erase Wu Line from (star_screenx(i), star_screeny(i)) to (star_Oldscreenx(i), star_Oldscreeny(i))
  
 	  (move the star closer)
	  star_z(i) = star_z(i) - star_zv(i);

	  (calculate screen coordinates)

 	  x = star_x(i) / star_z(i) * 100 + Center_of_screen_X
	  y = star_y(i) / star_z(i) * 100 + Center_of_screen_Y

	 (draw in the star at the new position)


	  (firstly, calculate the length of the streak)
          xd = x - star_screenx(i)
          yd = y - star_screenx(i)
          length = square_root( xd*xd + yd*yd )

	  (calculate the brightness of the star due to it's speed and distance)
	  b = (5000*starzv(i)) / starz(i)
	  
          (if the star is producing motion blur, dim it)
	  if length > 1 then b = (b/length)

	  draw a WuLine from (x, y) to (star_screenx(i), star_screeny(i)) with brightness b

          star_Oldscreenx(i) = star_screenx(i)
          star_Oldscreeny(i) = star_screeny(i)
          star_screenx(i) = x
          star_screeny(i) = y


Non-linear motion
Traveling forwards at great speed is fun for a couple of seconds, however traveling at 80 miles per hour is more fun on a roller coaster than in a car. Next, we'll try rotating the camera around a little as it flies along.

On each frame, you will need to update the values of angleX, angleY and angleZ. New bits are shown in bold.


.
.
[snip]
.
.
 (velocity of stars)
  array of floats: star_zv(0 to 63)

  Center_of_screen_X = 160
  Center_of_screen_Y = 100

 (Camera Movement)
  a = .00001


 (rotations of the camera)
  float: angleXvel = 0
  float: angleYvel = 0
  float: angleZvel = 0
  float: angleX    = 0
  float: angleY    = 0
  float: angleZ    = 0

program

 (initialise positions of stars)
  loop i from 0 to 63
      star_x(i) = random number between -1000 and 1000;
      star_y(i) = random number between -1000 and 1000;
      star_z(i) = random number between  100 and 1000;
      star_zv(i)= random number between  .5 and 5;
  end of i loop


 (now draw the starfield)
  loop forever


      (alter the rotation acceleration of the camera)
      angleXacc = angleXacc + some small random value between -a and a.
      angleYacc = angleYacc + some small random value between -a and a.
      angleZacc = angleZacc + some small random value between -a and a.

      (accelerate the camera)
      angleXvel = angleXvel + angleXacc
      angleYvel = angleYvel + angleYacc
      angleZvel = angleZvel + angleZacc

      (damp the motion of the camera)
      angleXacc = angleXacc * damping
      angleYacc = angleYacc * damping
      angleZacc = angleZacc * damping
      angleXvel = angleXvel * damping
      angleYvel = angleYvel * damping
      angleZvel = angleZvel * damping

      (move the camera)
      angleX = (angleX + angleXvel) * damping
      angleY = (angleY + angleYvel) * damping
      angleZ = (angleZ + angleZvel) * damping


      loop i from 0 to 63
.
.
[snip]
.
.

Then, you'll need to rotate each star before projecting it to the screen:


.
.
[snip]
.
.
      loop i from 0 to 63

	 (erase the old position of the star)

	  .
	  .
	  .

	  (calculate screen coordinates)

	  temp_x = star_x(i)
	  temp_y = star_y(i)
	  temp_z = star_z(i)

          Rotate (temp_x, temp_y, temp_z) by angleX degrees in the X-axis		
          Rotate (temp_x, temp_y, temp_z) by angleY degrees in the Y-axis		
          Rotate (temp_x, temp_y, temp_z) by angleZ degrees in the Z-axis		

 	  star_screenx(i) = temp_x / temp_z * 100 + Center_of_screen_X
	  star_screeny(i) = temp_y / temp_z * 100 + Center_of_screen_Y

	  if (star_screenx(i), star_screeny(i)) is off then screen, 
	     or star_z(i) < 1 then

              star_x(i) = random number between -500 and  500;
              star_y(i) = random number between -500 and  500;
              star_z(i) = random number between  100 and 1000;

	  end if

	 (draw in the star at the new position)
.
.
[snip]
.
.


The Final Result
So, after that's all that's been done, the finished product is quite effective. You can watch it for ages and drift off into some kind of semi-relaxed, altered brain state.

Source to Stars5