Ok... I hope you all managed to understand my VBL explanation... so as promised, here is the 4th tutorial: 2d stars!
So let's have a look at this over-used "old-school" effect and see how we can get a modern implementation for it using OpenGL.
First of all, this tutorial is the first to use the VBL/delta-timing mechanism explained in TUTORIAL #3. So if the words VBL and delta-timing mean nothing to you, please read TUTORIAL #3.
Stars.... what is it all about? Let's start with some background explanation about this classic effect.
On old computers like the C64 or the Amiga, this effect was achieved using sprites. A sprite was a small graphical object that unlike classic bitmaps had some intersting properties:
- fixed dimensions
- own set of colors
- ability to detect collisions with bitmaps
- independent control from bitmaps.
As explained in the TUTORIAL #2, these old computers had a way to control the graphics and change their properties at every scanline, simply by waiting the refresh mechanism to reach a specific scanline, change the contents and continue in the same way with other scanlines. What's the relationship with our stars??
2D stars were achieved by simply using one or more sprites and changing their x position and color at some specific scanlines, giving the illusion of several layers of dots moving horizontally, like stars would be seen in the sky through the window of a very fast moving spaceship. This effect was not only used in demos and intros but also in games (especially shoot'em ups such as Delta and the classic R-Type).
How do we implement such a mechanism with OpenGL? well, we do not! OpenGL does not know anything about sprites. It can just render primitives in a frame buffer. One of these primitives is the point. Mathematically speaking, a point does not have any dimension. However, OpenGL is nice to us and allows us to set a "size" for a point and also a color (amont some other properties).
If we can draw a point, we can draw several and actually OpenGL is very good at this. The point primitive is hardware accelerated and is a very good choice to simulate the old-schoold 2d starfield effect.
Where do we start?
First of all we define a structure that will be used for each one of our stars:
/*
definitionof a "star"
*/
typedef struct _Star2D
{
CFloat32 x;
CFloat32 y;
CByte color;
CByte speed;
} Star2D, *pStar2D;
As you can see each star will have a x and y coordinate a color and a speed value. Several stars having the same value for the color and for the speed, will move together. If we have different speeds, we will have several "layers" of points moving at different speeds, thus giving the illusion of a starfield.
How do we manage those stars?
We define an "object" that will manage them:
/*
the "object" that manages all the stars
*/
typedef struct _Stars2DObject
{
CInt32 min_x;
CInt32 max_x;
CInt32 min_y;
CInt32 max_y;
CuInt32 count;
CuInt32 planes;
CInt32 speed;
pStar2D stars;
CByte planeColors[8][3];
} Stars2DObject, *pStars2DObject;
This object will hold the minimum and maximum x as well as y coordinates that will define the area of our starfield. It will also hold the count of stars to be displayed, the number of planes, a general speed factor that will be applied to the different planes (allowing a single point of control for layers of stars with different speeds) and of course the colors for the stars, per plane. Chosing the colors so that the slowest plane is displayed with a dark color and the fastest plane with a light color will give the illusion of depth and.. space!
We now need some functions to manage this "object", here they are:
to create/initialize the starfield:
CBool stars2d_create(pStars2DObject obj);
to destroy/discard any allocated resource:
CBool stars2d_destroy(pStars2DObject obj);
to process the motion changes for each star:
CBool stars2d_process(pStars2DObject obj, CFloat32 deltaTime);
and finally to draw the stars:
CBool stars2d_draw(pStars2DObject obj, CuInt32 winWidth, CuInt32 winHeight, CuInt32 left, CuInt32 top, CuInt32 scaleFactor);
Let's have a look at the implementation:
1) Creating/Initializing the stars
/*
stars2d_create()
create stars
*/
CBool stars2d_create(pStars2DObject obj)
{
CuInt32 c;
/* allocates memory for the requested number of
stars. this buffer contains the properties of each star we want to display
*/
obj->stars=malloc(C_SIZE_OF(Star2D)*obj->count);
/* has the buffer? */
if (obj->stars)
{
CuInt32 width;
/* compute the x range for the stars */
width=MAX(obj->min_x,obj->max_x)-MIN(obj->min_x,obj->max_x);
/* generate some pseudo coordinates & properties for each star */
for (c=0; c < obj->count; c++)
{
/* generate a random x,y position within the give bounds */
obj->stars[c].x=(CFloat32)(obj->min_x+rand()%width);
obj->stars[c].y=(CFloat32)(obj->min_y+(rand()%(obj->max_y-obj->min_y)));
/* pick a random speed for this star, but with respect to the number of star planes requested */
obj->stars[c].speed=(CByte)(1+(rand()%obj->planes));
/* assign a color index to the star based on it's speed (i.e. depending to which plane it belongs) */
obj->stars[c].color=(CByte)(obj->stars[c].speed-1);
}
}
else
{
/* sorry, cannot allocate stars */
obj->count=0;
}
/* done */
return(TRUE);
}
Here we allocate a buffer that will hold a star structure as defined above, for each one of our stars. In this way we can manage them independently. Star position is picked at random within the min/max x and y. Then the speed is picked up at random among the number of planes we have. The color is dependent on the plane number. This allows us to create the illusion of depth by proving colors with an increase in intensity.
2) Cleaning the starfield object
/*
stars2d_destroy()
destroy stars
*/
CBool stars2d_destroy(pStars2DObject obj)
{
/* do we have the stars buffer? */
if(obj->stars)
{
/* discard the buffer */
free(obj->stars);
}
/* done */
return(TRUE);
}
This piece of code is called at the end, when we do not need the starfield anymore. Since a buffer has been allocated to hold the stars and their properties, we need to free that buffer when no longer needed.
3) Processing motion for the stars
/*
stars2d_process()
process stars (move stars)
*/
CBool stars2d_process(pStars2DObject obj, CFloat32 deltaTime)
{
CuInt32 c;
CuInt32 width;
/* compute maximum x range */
width=MAX(obj->min_x,obj->max_x)-MIN(obj->min_x,obj->max_x);
/* is speed positive? */
if (obj->speed > 0)
{
/* check for every star if it needs to have its x position reset */
for (c=0; c < obj->count; c++)
{
/* move star according to it speed and the delta time */
obj->stars[c].x -= ((CFloat32)(obj->stars[c].speed*obj->speed))*deltaTime;
/* is out of the screen? */
while (obj->stars[c].x < (obj->min_x))
{
/* reset its position */
obj->stars[c].x += width;
}
}
}
else
{
/* check for every star if it needs to have its x position reset */
for (c=0; c < obj->count; c++)
{
/* move star according to it speed and the delta time */
obj->stars[c].x -= ((CFloat32)(obj->stars[c].speed*obj->speed))*deltaTime;
/* is out of the screen? */
while (obj->stars[c].x > (obj->max_x))
{
/* reset its position */
obj->stars[c].x -= width;
}
}
}
/* done */
return(TRUE);
}
In this function we simply add the speed to each star in our array of stars. The speed depends on each star and is stored in the star structure. We compensate the speed by the value provided by our delta-timer/VBL mechanism, so the stars move at the same speed, whatever the computer used is. Eventually we check that if a star gets out of the visible area, it is reset on the other side, thus providing the illusion of scrolling stars.
4) Painting the stars in the sky
/*
stars2d_draw()
draw 2d stars
*/
CBool stars2d_draw(pStars2DObject obj, CuInt32 winWidth, CuInt32 winHeight, CuInt32 left, CuInt32 top, CuInt32 scaleFactor)
{
CuInt32 c;
/* set 2d mode */
gltk_tools_set_2d_mode(winWidth,winHeight);
/* disable any texturing, blending, etc */
glDisable(GL_TEXTURE_2D);
glDisable(GL_BLEND);
glDisable(GL_DEPTH_TEST);
glDisable(GL_ALPHA_TEST);
/* set the point size to the scale factor (in case we simulate low resolutions) */
glPointSize((CFloat32)scaleFactor);
/* begin to plot points (our actual stars) */
glBegin(GL_POINTS);
/* plot them all! */
for (c=0; c < obj->count; c++)
{
/* set color according to the star properties */
glColor4f(
((CFloat32)obj->planeColors[obj->stars[c].color][0])/256.0f,
((CFloat32)obj->planeColors[obj->stars[c].color][1])/256.0f,
((CFloat32)obj->planeColors[obj->stars[c].color][2])/256.0f,
1.0f
);
/* output a point at the star x,y position */
glVertex2f(left+obj->stars[c].x,top+obj->stars[c].y);
}
/* end of plotting */
glEnd();
/* done */
return(TRUE);
}
Drawing the stars is pretty simple. We set a 2D projection, disable any texturing, blending, etc. Then we loop through the stars and for each of them we plot a point using the GL_POINT primitive and of course the star's specific color.
5) calling the star routines from the main program
/* RETRO-REMAKES TUTORIAL #4 - 2D Stars
Written by Stormbringer (stormbringer@retro-remakes.net)
June 16, 2009
This tutorial demonstrates how to implement an "old-school" effect often called "2d stars" or "2d starfield"
It basically display moving dots in the x direction with different sets of speed thus creating the illusion of
a starfield moving horizontally. The implementation here uses the OpenGL point primitive to take advantage of the
hardware acceleration.
*/
/*
include the GLTK (Graphics Library Toolkit)
*/
#include "..\gltk\gltk.h"
/* routines
*/
/* background copperlist manager
*/
#include "..\routines\copper_list_bkg.h"
/* 2d stars manager
*/
#include "..\routines\stars_2d.h"
/* objects
*/
/* the copper list object
*/
CopperListBkgObject copperBkg_obj;
/* the starfield object
*/
Stars2DObject stars_obj;
/*
raster table for the "copper list"
see tutorial #2
*/
CopperListBkgEntry copperBkgList[]=
{
0,0,0,0,
20,255,255,255,
21,0,0,17,
220,255,255,255,
221,0,0,0,
};
/*
our screen "object" that holds the properties of our screen/window
*/
GLTKScreenProperties screenProperties;
/*
intro_on_mouse_left_down()
user pressed the left mouse button
*/
CError intro_on_mouse_left_down(pCVoid userData)
{
/* quit */
gltk_screen_quit();
/* done */
return(0);
}
/*
intro_on_vsync()
this method works like a timer interrupt. we will use it to track time changes and update the animation of or elements, etc
*/
CError intro_on_vsync(pCVoid userData, CFloat64 deltaTime)
{
if ((screenProperties.vsyncFPS == screenProperties.vsyncActual) && screenProperties.vsync)
{
stars2d_process(&stars_obj,(CFloat32)1.0f);
}
else
{
CFloat32 timeValue;
timeValue=(CFloat32)deltaTime*screenProperties.speedFactor*((CFloat32)screenProperties.vsyncFPS);
/* process objects */
stars2d_process(&stars_obj,(CFloat32)timeValue);
}
/* done */
return(0);
}
/*
intro_on_paint()
this method is used to paint the graphic elements on screen
*/
CError intro_on_paint(pCVoid userData)
{
/* clean the screen
*/
/* black background */
glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
/* clear depth buffer with default value */
glClearDepth(1.0f);
/* disable the depth test */
glDisable(GL_DEPTH_TEST);
glDepthFunc(GL_LEQUAL);
/* disable any clippin */
glDisable(GL_SCISSOR_TEST);
/* draw the copper list
*/
copper_list_bkg_draw(
&copperBkg_obj,
screenProperties.width,
screenProperties.height,
0,
0,
2 /* scale factor change this, depending on which resolution you want to simulate */
);
/* draw the stars */
stars2d_draw(
&stars_obj,
screenProperties.width,
screenProperties.height,
0,
0,
2 /* scale factor change this, depending on which resolution you want to simulate */
);
/* done */
return(0);
}
/*
entry point
*/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
/* hide cursor */
gltk_screen_show_cursor(FALSE);
/* init screen structure */
memset(&screenProperties,0,C_SIZE_OF(screenProperties));
/* name of the */
screenProperties.name="My new cracktro";
/* define window dimensions
*/
screenProperties.width=640;
screenProperties.height=480;
/* request vsync
NOTE: in order for this option to work fine, you need to set your graphics driver Vertical Sync option on "Application Controlled"
*/
screenProperties.vsync=FALSE;
/* request refresh rate
NOTE: this will only work 100% in full screen mode, if the graphics card is capable of it
*/
screenProperties.vsyncFPS=60;
/* flag for window mode/full screen mode
*/
screenProperties.fullScreen=TRUE;
/* our method for painting the screen
*/
screenProperties.paintMethod=intro_on_paint;
/* our method for managing the time (where we change animation, etc)
*/
screenProperties.vsyncMethod=intro_on_vsync;
/* our method to intercept the left mouse button
*/
screenProperties.leftMouseDown=intro_on_mouse_left_down;
/* init our copper list here
*/
/* number of entries in our list */
copperBkg_obj.count=C_SIZE_OF(copperBkgList)/C_SIZE_OF(copperBkgList[0]);
/* address of the list */
copperBkg_obj.list=copperBkgList;
/* init out starfield object
*/
memset(&stars_obj,0,C_SIZE_OF(stars_obj)); // clear the structure, make sure what's not used is set to 0
stars_obj.min_x=0; // stars start at the leftmost x coordinate
stars_obj.max_x=screenProperties.width; // stars get out at the rightmost x coordinate
stars_obj.min_y=22*2; // start y just below the first white raster line
stars_obj.max_y=219*2; // end y just above the second white raster line
stars_obj.count=200; // the total number of stars
stars_obj.planes=3; // 3 planes (layers) of stars
stars_obj.speed=3; // speed factor. the slowest plane of stars moves at 1 * stars_obj.speed,
// the second at 2 * stars_obj.speed, etc
/* RGB color of the slowest star plane */
stars_obj.planeColors[0][0]=64;
stars_obj.planeColors[0][1]=64;
stars_obj.planeColors[0][2]=64;
/* RGB color of the "middle" star plane */
stars_obj.planeColors[1][0]=180;
stars_obj.planeColors[1][1]=180;
stars_obj.planeColors[1][2]=180;
/* RGB color of the fastest star plane */
stars_obj.planeColors[2][0]=255;
stars_obj.planeColors[2][1]=255;
stars_obj.planeColors[2][2]=255;
/* create the starfield */
stars2d_create(&stars_obj);
/* open the screen/window using the give properties
*/
if (gltk_screen_open(&screenProperties))
{
/* run the main loop, intercept system events, etc
*/
gltk_screen_run_loop(&screenProperties);
/* close screen window
*/
gltk_screen_close(&screenProperties);
stars2d_destroy(&stars_obj);
}
/* exit and return code to system */
return(0);
}
And this is the main program, with the calls to our 2d starfield manager functions. The stars manager is initialized in the program's entry point just before we open the screen/window. Drawing occurs after we draw the background rasters. Finally and the most important, the processing of the motion is done by calling the processing function within the vsync method. As explained in TUTORIAL #3, we make the difference between the ideal case and the general case. It would not really be necessary for a simple 2d starfield effect as demonstrated here. However for the sake of being coherent, we do it.
Happy demo making now!