've been working on building a sprite-based 3d engine (think galaxy force 2) and was looking around for some help with the math for my projection when I found this site: http://www.extentofthejam.com/pseudo/ Did a quick search and didn't find it'd ever been posted here. It's a break down of the math and logic behind outrun style roads, going into detail about hardware specific methods of implementation. Really good read, it appears the author consulted the researchers behind cannonball to make sure a lot of their math is accurate. Might be helpful for anybody attempting to make a 3d engine without using any 3d hardware or libraries.
Cannonball run is open source, and I would expect it behaves similar to the methods he describes. Regardless, here's some allegro code that works very similarly: Code (Text): #include "allegro.h" #include <math.h> void DrawPerspective(BITMAP *t, BITMAP *tex, int y1, float z1, int y2, float z2, float x, float xmul, float ymul, float texoff) { float curvdiv = z1/(3.141592654f*0.5f); float zpos = 1.0f /z2; z1 = 1.0f / z1; /* sample 16 times for each scanline */ y1 <<= 4; y2 <<= 4; ymul *= 16.0f; int BottomY = y2; float zadd = (z1 - zpos) / (float)(y2 - y1); while(y2 > y1) { float z = 1.0f / zpos; /* NOTE: z is the actual depth of this scanline, zpos is 1/z. The most common perspective projection formula is used, I.e. transformed_x = (centre of screen) - (half width of screen)*(x / z) But instead of dividing by z we can multiply by zpos if we like. */ float centx = x + xmul*(cos(z/curvdiv) - 1.0f); int topy = y2 + (SCREEN_H >> 1)*(ymul*(cos(z/curvdiv) - 1.0f) / z); if((topy >> 4) < (BottomY >> 4)) /* question is: are we at least one pixel line above what was last drawn ? */ { stretch_blit(tex, t, 0, (int)((z+texoff)*4.0f)&(tex->h-1), tex->w, 1, (SCREEN_W >> 1) + (SCREEN_W >> 1)*(centx-1.0f)*zpos, (topy) >> 4, (SCREEN_W >> 1)*(2.0f)*zpos, (BottomY >> 4) -(topy >> 4)); BottomY = topy; } zpos += zadd; y2--; } } int main(int argc, const char *argv[]) { allegro_init(); install_keyboard(); if (set_gfx_mode(GFX_AUTODETECT_WINDOWED, 640, 480, 0, 0)) { allegro_message("Error setting 640x480 gfx mode:\n%s\n", allegro_error); return -1; } BITMAP *back = create_bitmap(SCREEN_W, SCREEN_H); /* obviously it would be better to load a road texture here, but we'll just generate a chequered pattern ... */ BITMAP *tex = create_bitmap(8, 2); int xp, yp; for(xp = 0; xp < tex->w; xp++) for(yp = 0; yp < tex->h; yp++) putpixel(tex, xp, yp, (xp+yp)&1 ? makecol(255, 255, 255) : makecol(0, 0, 0)); /* seed some variables */ float x = 0, xmul = 0, ymul = 0, z = 0; while(!key[KEY_ESC]) { clear_to_color(back, makecol(0, 0, 0)); DrawPerspective(back, tex, SCREEN_H >> 1, 20, SCREEN_H, 1, x, xmul, ymul, z); blit(back, screen, 0, 0, 0, 0, back->w, back->h); if(key[KEY_LEFT]) x+= 0.05f; if(key[KEY_RIGHT]) x-= 0.05f; if(key[KEY_UP]) z+= 0.05f; if(key[KEY_DOWN]) z-= 0.05f; if(key[KEY_P]) xmul-= 0.1f; if(key[KEY_O]) xmul+= 0.1f; if(key[KEY_A]) ymul-= 0.1f; if(key[KEY_Q]) ymul+= 0.1f; } destroy_bitmap(back); destroy_bitmap(tex); return 0; } END_OF_MAIN() produces this: Courtesy of Thomas Harte over at gamedev.net
Very interesting! I myself and pretty fascinated by pre-polygon 3D. I'm making a Wolfenstein 3D raycaster style game.
I've never actually made a raycaster game myself, although I've studied the logic behind them. Sounds like a neat project, do you maintain a blog or anything? I'd love to read your progress.
I post updates on twitter sometimes. To be fair, the actual raycasting part isn't really the hard part, the game logic is; my game runs really slow right now There's a really good tutorial on raycasting here.
I'm surprised that the linked page didn't examine OutRun 2016. That game featured some really cool intertwined roads.