# VBfx / Common / Starfield

## Introduction

There's a Starfield simulation screensaver coming with any Windows version down to 3.11 but it doesn't look too good. If you ever wondered how to make your own starfield this is the right tutorial for you. It's in fact not even complicated nor do you need fancy mathematics. The whole trick to convert 3D coordinates into 2D coordinates is: NewX = x / z and NewY = y / z respectively.

Well, sure this is kind of a fake but that's more or less a true 3D starfield and it looks way better than the Windows one. Besides the code is much faster, you'll be able to fly through 20'000 stars fluently. Interested in the code? Well keep on reading...

## Theory

Now let's get into theory first. As alread mentioned this is quite a small project and that's good for you. Basically we just have a list of stars where each star has it's 3d position (that's x, y and z). We need about 3 functions, one that creates the stars, one that moves them (towards the screen) and one that redraws the screen when everything is ready. Creating the stars happens (of course) before everything. In the main loop we follow the classical steps "clear-process-update-draw", that means clearing the screen, processing user input, updating the data (move the stars towards screen) and then draw everything again. Hm wait - we don't have any input to process... so we just skip that part ;)

That's it, we're already done with theory - let's get started:

## The data type

We start out creating the essential things - the stars. Since our stars are just the same as 3D-pixels the definition is quite simple. Add this code to a new module in your empty project:

```'Type definition
Public Type tStar
x As Single
y As Single
z As Single
End Type

'Star data
Public StarCount As Long
Public Star() As tStar
```

First we do the data type definition where each tStar holds it's x, y and z position. The second part creates an open array to hold the star data we need later. Well I already told you we're going to use some API calls, so here they come. You can just add it somewhere in the same module:

```'Declares
Public Declare Function GetTickCount Lib "kernel32" () As Long

Public Declare Function SetPixelV Lib "gdi32" (ByVal iDC As Long, _
ByVal x As Long, ByVal y As Long, ByVal iColor As Long) As Long
```

Not too hard is it? GetTickCount should be well known and, oh, well, SetPixelV is just a more accurate version of SetPixel. It does exactly what it says - setting a pixel on the desired position. The DC property says where to draw the pixel, if you don't know about it read the BitBlt tutorial first (or just skip it, it's fairly easy to handle).

Now before continuing we have to define some options we want to use later. First we need a Speed variable telling the engine how fast to move on. Also we need some values to define the spread range of the stars. A small range means that all stars are coming "out of the middle", a larger range would spread them over the whole screen. You can later play with these values just as you like. Append the code to the declares-section of your module:

```'Setup
Public Speed as Single

```

Since we already started declaring variables why not get it all done now? Well it's not much more, all we need is the screen position and the screen middle (just for optimization). Of course the screen middle is nothing more than Width / 2, Height / 2 but the code looks cleaner if you put this in variables:

```'Screen size
Public ScreenW as Long
Public ScreenH as Long

'Screen middle
Public OffsetX as Long
Public OffsetY as Long
```

Wonder why it's called Offset? Well later we'll use the screen middle as the offset for drawing the stars. I'll get back to this later, just keep it in mind.

## Creating the stars

When I'm talking about star creating don't get confused because it sounds complicated. In fact it's nothing more than setting some random positions. The code also goes into your module:

```Public Sub SetupStars( iCount as Long )
Dim A as Long

'Create stars array
StarCount = iCount
ReDim Star( StarCount )

'Set random positions
For A = 0 To StarCount
With Star( A )
.x = ( Rnd - 0.5 ) * SpreadX
.y = ( Rnd - 0.5 ) * SpreadY
End With
Next
End Sub
```

I guess you wonder about the x = ( Rnd - 0.5 ) * SpreadX part? Let me explain shortly: Rnd returns a random number between 0 and 1 (like 0.02 or 0.87). Now since we're flying "through the middle" of the starfield (position 0, 0) we need to place the stars around the middle and the simplest way to do this is randomize between -0.5 and 0.5 and that's what (Rnd - 0.5) does. Multiplying this with any value we can scale the placement range just as we need it. An alternative way to write this would be: x = (SpreadX * Rnd) - (SpreadX / 2) - that's exactly the same but probably easier to understand (but the other code is shorter ;).

## Update scene - moving stars

Even if this sounds very complicated it's not! Moving the stars means nothing more than z = z - speed, really. The only thing we have to do is checking z to be smaller than 0. If it's below 0 the star would be "in front of the screen" and they are not supposed to be there. Erm wait, the stars won't jump out of the screen! The projection would just reflect them so they come towards the screen, bounce and then move backwards again.

However we don't want that so we have to prevent it. Since the stars would leave the screen, the user won't in any case see them any longer - therefore we can reset their positions so they appears again in the very back of the scene. I'm talking about re-using resources that are no longer displayed.

```Public Sub MoveStars()
Dim A as Long

For A = 0 To StarCount
With Star( A )
'Move towards screen
.z = .z - Speed

If .z <= 0 Then
'Reset star when in front of screen
.x = ( Rnd - 0.5 ) * SpreadX
.y = ( Rnd - 0.5 ) * SpreadY
End If
End With
Next
End Sub
```

This code also goes in your module. It does nothing fancy so I won't explain it any further. Just a little not about reseting the z-position: When creating the stars we randomized the value. Here we can't (or shouldn't) because this would make the stars plop anywhere on the scene. Instead we want them to smoothly re-appear from the very back, therefore setting z to SpreadZ here which is the maximum for z-positions.

## Drawing the stars

I always talked about how easy the functions are, but not here, fear the draw-function *muahaha*. Ehm, wait, just kidding here...

On the other hand this is the main part of the starfield so keep your eyes open, you have to understand the following. I'm switching to line-by-line mode now, guiding you through the code.

```Public Sub DrawStars( DC as Long )
Dim A as Long

Dim TempX as Long
Dim TempY as Long
Dim TempColor as Single
```

Not too hard yet but let me explain the Temp values. Basically it's just everything we need for the SetPixel function now: x and y position and the pixel color. Putting this into temp variables makes your code cleaner and easier to extend later. Okay let's go on...

```    For A = 0 To StarCount
With Star( A )
'Get 2D (projected) position
TempX = ( .x / .z ) + OffsetX
TempY = ( .y / .z ) + OffsetY
```

Remember what I said before? The whole 2D projection happens here, this means converting 3D positions to 2D positions. And the formula - as I told you - is just NewX = x / z and NewY = y / z respectively.

Now have a look at the Offset (screen middle) variables. You can comment it out to see what happens: The middle of the starfield is in top-left corner of the screen. Why? Simply because the stars are placed around the 0-point (as mentioned when creating them). To fix the problem we just move the whole starfield to the middle of the screen and that's what adding the Offset does. Note that we can't add a pixel-based offset before the projection because the stars don't have pixel-based positions. In fact they only have relative coordinates because in 3D it's all about scaling.

```            'Get color from distance
TempColor = ( 1 - ( .z / SpreadZ ) ) * 255
```

I hope this isn't too hard to understand. It's a little trick to make the color of each star dependant of it's z-position. The z-value goes from 0 to SpreadZ, thus dividing z / SpreadZ returns values between 0 and 1 (playing with maths here). Now the far-away stars would be white, getting darker as the come nearer. To invert this behaviour we need the 1 - x statement before the bracket. And finally we have to multiply the whole thing so we can feed the RGB-function with this values.

```            'Draw pixel
SetPixelV DC, TempX, TempY, RGB( TempColor, TempColor, TempColor )
End With
Next
End Sub
```

The last step should be clear, putting everything together by calling the SetPixel function, passing the TempX, TempY position and the color we get from the RGB function.

## The final step

Now that we got all the functions I talked about it's time for the last part of this tutorial: Setup the whole thing and running the main loop.

I put the whole setup in Form_Load, shouldn't bee too hard to understand:

```Private Sub Form_Load()
'Setup values
Speed = 0.1

'Setup screen
Me.Move 0, 0, Screen.Width, Screen.Height
Me.Show

'Get size
ScreenW = Me.ScaleWidth
ScreenH = Me.ScaleHeight

'Get screen middle
OffsetX = ScreenW / 2
OffsetY = ScreenH / 2

'Initialize data
SetupStars 10000

'Call main loop
Main
End Sub
```

Speed should be clear, erm, yes. As mentioned at the beginning you can chose any values for the Spread variables. Try larger and smaller ranges to see the effect. Note that a wider spreading demands more stars if you still want the screen to be filled.

The main function that is called in the last line looks as follows. It's controlled by a simple GetTickCount timer, making sure it runs at 30 frames / second:

```Public Sub Main()
Dim NextTick as Long

'Init timer
NextTick = GetTickCount

While DoEvents
If GetTickCount > NextTick Then
'Set wait interval
NextTick = GetTickCount + ( 1000 / 30 ) 'About 30 FPS

'Clear the screen
Me.Cls

'Move all stars
MoveStars

'Draw stars and update screen
DrawStars Me.hdc
Me.Refresh
End If
Wend
End Sub
```

Finally we need something to exit the program - The escape key will do the job for us, make it work by adding this line of code to the Form:

```Private Sub Form_KeyDown( KeyCode as Integer, Shift as Integer )
If KeyCode = vbKeyEscape Then: Unload Me
End Sub
```

...should be clear.

Now wait! Before pressing the "Run" button now make sure your Form is setup correctly! Set the BackgroundColor to full black and the ScaleMode property to 3 - vbPixels. Also important: Set the BorderStyle property to 0 - none, otherwise it won't run in fullscreen mode. To prevent flickering also set AutoRedraw to true. Note that this only works because we added the Me.Refresh statement to update the screen (otherwise nothing would show up).

You can even add a background picture if you like.

## Running the project

If you did everything correctly this it the moment of interest. Run the project and fly through the stars!

A little side note: Instead of just using Me.Cls to clear the screen (Cls meas Clear screen) you could also overdraw each pixel by it's original color. To achieve this you have to store the original pixel's color before drawing it and (for speed reasons) the projecte 2D-position so you can overdraw it later without re-calculating the projection again. If you're interested in this technique check out the second example project called Starfield2. Overdrawing is faster in some cases, however as soon as you have a lot of stars Me.Cls works faster again.

## Conclusion

In this tutorial you learned the basics steps of an engine: Setup, then clear-input-update-draw and finally release everything (we skipped some parts but you can imagine ;). You also came in touch with the projection formula which is fairly simple. However this wasn't the whole truth - you can for example extend the projection by a FoV (field of view) range or additional corrections. When working with DirectX you'll meet these things again but for the moment it's just fine.