A Space Trading Sim is born
3D Game programming for beginners

Episode 1: The real world and your flat screen

Written by Kevin R. (August, 2007)


Welcome everyone to the first tutorials in this series which is aimed at beginners to 3D programming and maybe to game programming. I know many people (at least 90%) will skip this introduction so I will keep it short. All the code in this text is written for FreeBasic. I will not use any external package or library such as DirectX or OpenGL... That was shocking... No DirectX or OpenGL, so how we gonna produce some 3d graphics??? Simple... with our own FBgfx.

Why would I ignore DirectX or OpenGL and don't use them??? Every game produced nowadays uses them. I can program applications using OpenGL and DirectX, so that's the problem neither. The answer is simple and summarised with one word: understanding.

Understanding the concepts which are working within 3D engines is very important and can make your life much easier. When you write applications with OpenGL and use matrix equation, rotation and shading commands, these commands are black boxes. You know they produce nice graphics, but how they do it is unknown. The big problems arrive when you get a nasty runtime error somewhere in the rendering of your flashy games. Good understanding is important. Besides that, itís fun to look if your routine is faster than DirectX or OpenGL.

What we are going to do?

In this series Iím planning to do a space trading sim like Elite or some more up-to-date Freelancer. This type of game can easily be separated in little pieces for a tutorial in order that at the end of the series we got an entire game ready and running.

This episode I will explain the conversion from 3D to a 2D screen, the base of an 3d engine. Not very difficult and for some maybe way to easy, but look at the top of the text:
3D game programming for BEGINNERS, so I will start at lowest level possible. Later on in the series I will get to more difficult problems.

Alright, what do we need in random order:

2D and 3D worlds

Alright, what is the difference between 2D and 3D worlds? Or in gamer language, what is the difference between Super Mario and Quake? The answer is again simple: a coordinate. In a 3D world we define a point by 3 coordinates respectively (x, y, z). In 2D we use 2 coordinates (x, y). So we've to find a way to convert a 3D coordinate to a 2D coordinate.

Before we can do that, we have to define the 3D coordinate system. I'm using a right handed system:
z-axis into the screen, y up the screen and x positive to the right. This is called a right handed system because when you point your thumb of your right hand to the screen, your four fingers point to the direction of positive rotation. This sounds like hocus-pocus right know, I know... But I will explain it later in a tutorial on rotations. For now just assume z into the screen, y upwards and x to the right, OK?

Well, that's done. Now take a good look at the screen in front of you. It is flat. How we are going to simulate a 3D effect on this screen? The screen has only x and y coordinates, so we need a conversion which is depending on the z-coordinate. Could it be so easy...? Well actually it is... Look at it this way: a point is defined as (x,y,z). We can multiply this point by a constant.

constant * (x, y, z)

What value must this constant have in order to make the formula depending on z but staying in a 2D plane? Again simple:

constant = 1 / z because (1 / z) * (x, y, z) = (x / z, y / z, 1) 

As you see x and y are variable but the z coordinate always equals 1 thus for all z only x and y can vary, but the plane remains the same.

So we are working currently at the plane z = 1. If we want to change this plane, to make it further away or closer by we simply change the constant a little:

constant = b / z

Where b can be every value. With b you can adjust the perspective. Usually values like 128, 256 or 512 are used for b, but I encourage you to experiment with this value to achieve a desired effect or view. From here on b is called the scale.

The transformation can now be done with:

x2d = x3d * Scale / z3d + (screen width / 2)
y2d = y3d * Scale / z3d + (screen height / 2)

First one note to the transformation formulas. The first runtime error which can occur and is sometimes hard to find is the division by zero: If z3d equals zero we get and error so be careful. Second note is the screen height and screen width used in the formulas. It's common in 3D engines to use the middle point of the screen as the origin. So if you are using a 640 x 480 resolution the origin lies at (320, 240) which is (0,0,z) in 3D.

I know that the information above looks simple at a first glance, but I bet (and I was one too) that you have to think hard to understand the concept. Therefore I present to you all: Some source code!!!

' Screen dimensions of mode 13
Const SCRN_X = 320
Const SCRN_Y = 200

Type Point3D
	x As Integer
	y As Integer
	z As Integer
End Type

Type Point2D
	x As Integer
	y As Integer
End Type

Dim p3d As Point3D	' 3D point
Dim p2d As Point2D	' 2D point
Dim Scale As Integer	' Perspective variable

' Define perspective
Scale = 256
' Define starting coordinates
p3d.x = 1
p3d.y = 1
p3d.z = 256

' For sake of simplicity i use screen 13 (320 x 200)
' Change for other resolution. (don't forget the constants)
Screen 13

Do While (p2d.x < SCRN_X) and (p2d.x > -1) and (p2d.y < SCRN_Y) and (p2d.y > -1)
	' Convert 3d to 2d
	p2d.x = p3d.x * Scale / p3d.z + (SCRN_X / 2)
	p2d.y = p3d.y * Scale / p3d.z + (SCRN_Y / 2)
	' Put pixel
	PSet (p2d.x, p2d.y), 40
	' reduce z and check if z > 0 (no division by zero)
	p3d.z = p3d.z - 1
	If p3d.z = 0 Then p3d.z = -1	

Loop Until InKey <> ""

Well I think the code above speaks for itself. First two types are created for 2d points and 3d points. Then the perspective is determined as 256. I like this value, but feel free to choose your own. The loop contains only a z decrement and a conversion from 3D to 2D. This code is the base of the 3D starfield which I present to you below. This source is not optimised and if you like you can do that by yourself (a good exercise). Like I said before, this tutorial is for beginners and optimising is a subject that I may treat in one of the last tutorials.

' =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
'				3D Starfield
'				The Flipside
' =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

Declare Sub GenerateStar(index As Integer)

Const MAX_STARS = 500
Const SCRN_WIDTH = 320
Const SCRN_HEIGHT = 200
Const SPEED = 0.015      ' Adjust for different speed

Type Point3D
	x As Integer
	y As Integer
	z As Integer
End Type

Type Point2D
	x As Integer
	y As Integer
End Type

Dim shared Star(0 To (MAX_STARS - 1)) As Point3D
Dim p2d(0 To (MAX_STARS - 1)) As Point2D

Dim i As Integer
Dim Scale As Integer
Dim OldTime As double
Dim TimeDummy As double

' Initialize starfield and variables 
Randomize Timer					' Random seed
For i = 0 To (MAX_STARS - 1)
	GenerateStar(i)				' Create stars!!!

Scale = 256					    ' Perspective

' =-=-=-=-=-= Main Loop =-=-=-=-=-=
Screen 13 ' Screen mode 13, change if you like but don't forget the constants!

Do While Inkey <> Chr(27) ' Loop until esc is pressed
	For i = 0 To (MAX_STARS - 1)
		' Delete old star
		PSet (p2d(i).x, p2d(i).y), 0 ' draw a black pixel on old position
		' Transformation from 3D to 2D
		' Origin lies in the middle of the screen
		p2d(i).x = star(i).x * Scale / star(i).z + (SCRN_WIDTH / 2)
		p2d(i).y = star(i).y * Scale / star(i).z + (SCRN_HEIGHT / 2)
		' Draw star
		PSet (p2d(i).x, p2d(i).y), 15
		' reduce z and check if z > 0 (no division by zero)
		star(i).z = star(i).z - 1
		If Star(i).z <= 0 Then ' star has passed us, lets create a new one
	' Some timing code to reduce the speed of the starfield
	' Waiting time = SPEED seconds
	OldTime = Timer
		TimeDummy = Timer
        SLEEP 1
		' Does nothing but wait. (Old QB way of timing im always using)
		' I know there are better ways, but this works
	Loop Until (TimeDummy - OldTime) >= SPEED 

Sub GenerateStar(index As Integer)

	' The origin of the screen lies at the middle of the screen.
	' So in case of screen 13 the possible screen coordinates are:
	' -160 to 160, -100 to 100
	' This procedure divides the stars array in four and assign every
	' screen quadrant with it. Sounds complicated?
	' Look at it this way: 
	' The first IF puts one fourth of MAX_STARS in the box constaining
	' the coordinates (0, 0), (100, 160)
	' Then the ElseIf puts stars in the box containing the coordinates
	' (0, 0), (-160, 100)
	' And so on! Get it already???
	' If not change the signs before the (SCRN_WIDTH / 2) or (SCRN_HEIGTH / 2)
	' and see what happens!  
	If index < (MAX_STARS / 4) THEN
		Star(index).x = Rnd(1) * (SCRN_WIDTH / 2) + 1
		Star(index).y = Rnd(1) * (SCRN_HEIGHT / 2) + 1 
	ElseIf (index > (MAX_STARS / 4)) And (index < (MAX_STARS / 2)) Then
		Star(index).x = Rnd(1) * -(SCRN_WIDTH / 2) + 1
		Star(index).y = Rnd(1) * (SCRN_HEIGHT / 2) + 1
	ElseIf (index > (MAX_STARS / 2)) And (index < (3 * MAX_STARS / 4)) Then
		Star(index).x = Rnd(1) * (SCRN_WIDTH / 2) + 1
		Star(index).y = Rnd(1) * -(SCRN_HEIGHT / 2) + 1
	ElseIf (index > (3 * MAX_STARS / 4)) And (index < MAX_STARS) Then
		Star(index).x = Rnd(1) * -(SCRN_WIDTH / 2) + 1
		Star(index).y = Rnd(1) * -(SCRN_HEIGHT / 2) + 1
	Star(index).z = Rnd(1) * 256
End Sub

Closing down

Hope you liked this tutorial. I did while writing. I know that not much of true game code and game concepts are explained in this tutorial. That's something I leave to later tutorials. First Iím gonna explain some basic techniques and after that the game elements are introduced. However, don't be disappointed. This will be soon enough.

Some tips for experimenting:

Have fun and see you next time,

The Flipside


Proofreading and minor changes by Lachie Dazdarian - lachie13@yahoo.com