onsdag 25 februari 2009

Going 3D with Allegro 5


I decided to play with 3D in Allegro 4.9.x today.
It was really simple to get started, compared to other ways of doing it.
Allegro takes care of all the setup and stuff that is messy when you're trying to learn opengl.

This example code shows how simple it is to get going.
It loads a font and a texture using allegro and draws a bit in 2D and a bit in 3D.

I've only built this on Linux, you'll have to translate the linking to your favourite compiler yourself.
g++ Allegro3D.cpp `allegro5-config --libs` -la5_font-4.9.8 -la5_ttf-4.9.8 -la5_iio-4.9.8 -o Allegro3D



#include <cmath>
#include <allegro5/allegro5.h>
#include <allegro5/a5_iio.h>
#include <allegro5/a5_font.h>
#include <allegro5/a5_ttf.h>
#include <allegro5/a5_opengl.h>

ALLEGRO_BITMAP* texture = NULL;
ALLEGRO_FONT* font = NULL;
int width = 640;
int height = 480;

/* gluPerspective is a function that exists in an addon to gl.
* I put my own version in here to avoid messing with more libraries than neccesary.
*/
void gluPerspective(float FOV, float ASPECT, float NEARPLANE, float FARPLANE)
{
double left, right;
double bottom, top;
top = tan (FOV*3.14159/360.0)*NEARPLANE;
bottom = -top;
left = ASPECT*bottom;
right = ASPECT*top;
glFrustum (left, right, bottom, top, NEARPLANE, FARPLANE);
}

void Render()
{
//A bit of 2D drawing before venturing into 3D
double ms = al_current_time();
al_font_textout(font, 0, 0, "Goodbye 2D world", -1);
al_draw_bitmap(texture, 0, 20, 0);

//Set up perspective projection
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,0.1f,100.0f);

// Do your 3D stuff
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

glBindTexture(GL_TEXTURE_2D, al_get_opengl_texture(texture));
glEnable(GL_TEXTURE_2D);
glTranslatef(0, 0, -10);
glRotatef(ms*36, 1, 1, 1);
glBegin(GL_QUADS);
glTexCoord2f(0, 0); glVertex3f(-1, -1, 0);
glTexCoord2f(1, 0); glVertex3f(+1, -1, 0);
glTexCoord2f(1, 1); glVertex3f(+1, +1, 0);
glTexCoord2f(0, 1); glVertex3f(-1, +1, 0);
glEnd();

//Return to Allegros 2D world
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();

//Do some 2D stuff
al_font_textout_right(font, width, height-20, "Welcome back 2D world", -1);
}

int main()
{
al_init();
al_install_keyboard();
al_iio_init();
al_font_init();

ALLEGRO_DISPLAY *display;
al_set_new_display_flags(ALLEGRO_WINDOWED | ALLEGRO_OPENGL);
display = al_create_display(width, height);

ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();
al_register_event_source(event_queue, (ALLEGRO_EVENT_SOURCE *)display);
al_register_event_source(event_queue, (ALLEGRO_EVENT_SOURCE *)al_get_keyboard());

font = al_ttf_load_font("font.ttf", 20, 0);
texture = al_iio_load("texture.jpg");

double last_time = al_current_time();

bool quit = false;
while(!quit)
{
ALLEGRO_EVENT event;
if (al_get_next_event(event_queue, &event))
{
if (ALLEGRO_EVENT_KEY_DOWN == event.type && ALLEGRO_KEY_ESCAPE == event.keyboard.keycode
|| ALLEGRO_EVENT_DISPLAY_CLOSE == event.type)
{
break;
}
}

al_clear(al_map_rgb(0, 0, 0));
Render();
al_flip_display();

al_rest(0.001);
}

al_destroy_event_queue(event_queue);
al_destroy_display(display);
}
END_OF_MAIN()

söndag 21 december 2008

Boosted resource manager

This entry has been slightly delayed because I was yet again pissed of by how poorly webstuff works. This time it was the <'s and >'s in my code that gave me a headache. Thus I put off posting this for a few days until I got my rational mind back. The solution was actually quite simple, I just had to use sed.

Anyway time to get on topic. For some reason I felt compelled to add a resource manager to a project I'm working on. I already had one ready to be used, but I felt it was too simple. It didn't have any features other than keeping track of what resources had already been loaded so you avoid duplicates.

So I wanted to add a little to it. First of all I felt the need to use boost::shared_ptr since that is quite a bit safer than the raw pointers it was using. This was not entirely trivial since the resource manager manages more than one type of resource. I could not store different types of shared pointers in the same resource map. Therefore I turned to the boost::any type which can store anything. I must say it solved my problem quite nicely.

Secondly I felt it might be a good idea to add a Drop function so you can tell the resource manager to drop your resource if you don't want it to stay loaded when it's not used anymore.

Thirdly I wanted to add threading so you can load big/slow/web/complex resources and still be able to provide your user with program responsiveness or at least some animated eye candy. However, this part is not done yet, I am in the finishing stages of coding it but I thought I'd give it a whole new post because I don't want to clutter the simplicity of the threadless resource manager.

Well that's all I want to say about it for now. Enjoy the code!

#ifndef resource_manager_h
#define resource_manager_h

#include <map>
#include <string>
#include <typeinfo>
#include <iostream>
#include <boost/shared_ptr.hpp>
#include <boost/any.hpp>

typedef std::map<std::string, boost::any> Resource_map;

/* Class: Resource_manager
* Managing resources.
* */
class Resource_manager
{
public:
/* Function: Load
* Loads a resource, or returns it if it has already been loaded.
*
* Parameters:
* filename - File to load
*
* Returns:
* Shared pointer to loaded resource.
* */
template <typename T> boost::shared_ptr<T> Load(std::string filename)
{
typedef boost::shared_ptr<T> TSP;
std::string resname = filename + "*" + typeid(T).name();
Resource_map::iterator i = resources.find(resname);

if(i != resources.end()) {
try
{
TSP tsp = boost::any_cast<TSP>(i->second);
return tsp;
}
catch(boost::bad_any_cast e)
{
}
}

TSP resource = TSP(new T);
if(!resource->Load(filename)) {
std::cout << "Resource failed to load: " << filename << std::endl;
resource.reset();
return resource;
}
resources[resname] = resource;
return resource;
}

/* Function: Drop
* Drops a resource from the manager.
* This will not affect any shared pointers that are still pointing to it.
*
* Parameters:
* r - Shared pointer to the resource to drop.
* */
template <typename T> void Drop(boost::shared_ptr<T> r)
{
typedef boost::shared_ptr<T> TSP;
for(Resource_map::iterator i = resources.begin(); i != resources.end(); ++i)
{
try
{
TSP tsp = boost::any_cast<TSP>(i->second);
if(tsp == r)
{
resources.erase(i);
return;
}
}
catch(boost::bad_any_cast e)
{
}
}
}
private:
Resource_map resources;
};

#endif

måndag 8 december 2008

Scons and boost test

A few days a ago I decided I wanted to use boosts unit testing framework with scons. So I googled for some help on how to integrate boost.test with scons. This resulted in finding a wikipage at the scons website.

Some code for boost test was found under the title "Unit Test integration with an scons Tool". But that page didn't explain much, it threw some code at me and left it up to me to figure out where to put it. It took me about a day to solve all the issues, so now that I know how to do it I thought I'd share a more complete guide.

Note that this is just a guide on how to get it working. I haven't even explored any nifty modifications myself yet.

Step 1: In the main Sconstruct file
I assume you have an environment made (called env), the following code is added after that.
This script initiates a tool that compiles and runs your test cases.


### Testing ###
Export('env')
# Set up the test environment. We copy the environment so that we can add the
# extra libraries needed without messing up the environment for production
# builds.
#
# Here we use boost.test as the unit testing framework.
testEnv = env.Clone()
testEnv.Tool('unittest',
toolpath=['build_tools'],
UTEST_MAIN_SRC=File('build_tools/boostautotestmain.cpp'),
LIBS=['boost_unit_test_framework']
)
Export('testEnv')
# grab stuff from sub-directories.
env.SConscript(dirs = ['test'])


Step 2: The tests main function
Make a build_tools folder in your project folder.
Create the file boostautotestmain.cpp in that folder and put the following code in it.
Yes, this is the toolpath and UTEST_MAIN_SRC from step one, so you can adapt this if you don't like the naming.
This code is for creating the main function for your tests. I'm using the auto test cases.
If you want to run tests differently please read the boost docs or google for help.


#define BOOST_TEST_DYN_LINK
#define BOOST_TEST_MAIN
#include


Step 3: The tool script
In your project folder add the folder site_scons and under that the folder site_tools.
Then put the following code in unittest.py in there, so it's theproject/site_scons/site_tools/unittest.py.
That's not my idea, it's scons standard for adding local tools. If you want to make the tool global, please read scons docs.


import os
def unitTestAction(target, source, env):
'''
Action for a 'UnitTest' builder object.
Runs the supplied executable, reporting failure to scons via the test exit
status.
When the test succeeds, the file target.passed is created to indicate that
the test was successful and doesn't need running again unless dependencies
change.
'''
app = str(source[0].abspath)
if os.spawnle(os.P_WAIT, app, env['ENV'])==0:
open(str(target[0]),'w').write("PASSED\n")
else:
return 1
def unitTestActionString(target, source, env):
'''
Return output string which will be seen when running unit tests.
'''
return 'Running tests in ' + str(source[0])
def addUnitTest(env, target=None, source=None, *args, **kwargs):
'''
Add a unit test
Parameters:
target - If the target parameter is present, it is the name of the test
executable
source - list of source files to create the test executable.
any additional parameters are passed along directly to env.Program().
Returns:
The scons node for the unit test.
Any additional files listed in the env['UTEST_MAIN_SRC'] build variable are
also included in the source list.
All tests added with addUnitTest can be run with the test alias:
"scons test"
Any test can be run in isolation from other tests, using the name of the
test executable provided in the target parameter:
"scons target"
'''
if source is None:
source = target
target = None
source = [source, env['UTEST_MAIN_SRC']]
program = env.Program(target, source, *args, **kwargs)
utest = env.UnitTest(program)
# add alias to run all unit tests.
env.Alias('test', utest)
# make an alias to run the test in isolation from the rest of the tests.
env.Alias(str(program[0]), utest)
return utest
#-------------------------------------------------------------------------------
# Functions used to initialize the unit test tool.
def generate(env, UTEST_MAIN_SRC=[], LIBS=[]):
env['BUILDERS']['UnitTest'] = env.Builder(
action = env.Action(unitTestAction, unitTestActionString),
suffix='.passed')
env['UTEST_MAIN_SRC'] = UTEST_MAIN_SRC
env.AppendUnique(LIBS=LIBS)
# The following is a bit of a nasty hack to add a wrapper function for the
# UnitTest builder, see http://www.scons.org/wiki/WrapperFunctions
from SCons.Script.SConscript import SConsEnvironment
SConsEnvironment.addUnitTest = addUnitTest
def exists(env):
return 1


Step 4: Adding some tests
Make a file called SConscript in test folder, or whereever you have test sources.
The tool only processes tests that are in directories specified in the last line of the script in step 1.
And here's the code to put in SConscript.


#-------------------------------------------------------------------------------
# Unit tests
Import('testEnv')
Import('env')
testEnv = testEnv.Clone()
testEnv.AppendUnique(LIBPATH=[env.Dir('../lib')], LIBS=['rosal_renderer'])
testEnv.PrependENVPath('LD_LIBRARY_PATH', env.Dir('.').abspath)
# We can add single file unit tests very easily.
#testEnv.addUnitTest('test.cpp')
# also, multiple files can be compiled into a single test suite.
#files = Split('''
# one_test.cpp
# two_test.cpp
# ''')
#testEnv.addUnitTest('test_suite', files)
# you can also use glob to add all the files in the folder.
files = glob.glob('*.cpp')
testEnv.addUnitTest('test_suite', files)
# all the tests added above are automatically added to the 'test' alias.