#define USE_OSMESA 1 // Mesa is an open-source OpenGL implementation. // OSMesa is its off-screen driver, used for rendering into a memory buffer // rather than through a graphics card. This is great for making OpenGL work // pretty much on anything, including DOSBox on its emulated basic VGA hardware. // // You can download my DJGPP-compiled version of OSMesa at: // http://bisqwit.iki.fi/jutut/kuvat/programming_examples/djgpp_mesa.zip #define GL_GLEXT_PROTOTYPES #if USE_OSMESA # include // For everything OpenGL, but done all in software. #else # include #endif #include // GLU = OpenGL utility library // Standard C++ includes: #include // For std::min, std::max #include // For std::pow, std::sin, std::cos #include // For std::vector, in which we store texture & lightmap namespace PC { const unsigned W = 320, H = 200; const unsigned R = 7, G = 9, B = 4; // 7*9*4 regular palette (252 colors) //const unsigned R = 6, G = 7, B = 6; // 6*7*6 regular palette (252 colors) //const unsigned R = 6, G = 8, B = 5; // 6*8*5 regular palette (240 colors) //const unsigned R = 8, G = 8, B = 4; // 8*8*4 regular palette (256 colors) } // DJGPP-specific include files, for accessing the screen & keyboard etc.: #include // For kbhit, getch, textmode (console access) #include // For __dpmi_int (mouse access) #include // For _dos_ds (VRAM access) #include // For movedata (VRAM access) #include // For outportb (palette access) #include // For _farsetsel and _farnspokeb (VRAM access) namespace PC { const double PaletteGamma = 1.5; // Apply this gamma to palette const double DitherGamma = 2.0/PaletteGamma;// Apply this gamma to dithering unsigned char ColorConvert[3][256][256], Dither8x8[8][8]; const bool TemporalDithering = true; // Do temporal dithering const unsigned DitheringBits = 6; // Dithering strength unsigned ImageBuffer[W*H]; void Init() // Initialize graphics { // Create bayer 8x8 dithering matrix. for(unsigned y=0; y<8; ++y) for(unsigned x=0; x<8; ++x) Dither8x8[y][x] = ((x ) & 4)/4u + ((x ) & 2)*2u + ((x ) & 1)*16u + ((x^y) & 4)/2u + ((x^y) & 2)*4u + ((x^y) & 1)*32u; // Create gamma-corrected look-up tables for dithering. double dtab[256], ptab[256]; for(unsigned n=0; n<256; ++n) { dtab[n] = (255.0/256.0) - std::pow(n/256.0, 1/DitherGamma); ptab[n] = std::pow( n/255.0, 1.0 / PaletteGamma); } for(unsigned n=0; n<256; ++n) for(unsigned d=0; d<256; ++d) { ColorConvert[0][n][d] = std::min(B-1, (unsigned)(ptab[n]*(B-1) + dtab[d])); ColorConvert[1][n][d] = B*std::min(G-1, (unsigned)(ptab[n]*(G-1) + dtab[d])); ColorConvert[2][n][d] = G*B*std::min(R-1, (unsigned)(ptab[n]*(R-1) + dtab[d])); } // Set VGA mode 13h (320x200, 256 colors) textmode(0x13); // Will set graphics mode despite the name. // Set up regular palette as configured earlier. // However, bias the colors towards darker ones in an exponential curve. outportb(0x3C8, 0); for(unsigned color=0; color< R*G*B; ++color) { outportb(0x3C9, std::pow(((color/(B*G))%R)*1./(R-1), PaletteGamma) *63); outportb(0x3C9, std::pow(((color/ B)%G)*1./(G-1), PaletteGamma) *63); outportb(0x3C9, std::pow(((color )%B)*1./(B-1), PaletteGamma) *63); } __dpmi_regs regs = { }; regs.x.ax = 0; __dpmi_int(0x33, ®s); // Initialize mouse } void Render() // Update the displayed screen { static unsigned f=0; ++f; // Frame number _farsetsel(_dos_ds); for(unsigned y=0; y> DitheringBits)); if(!TemporalDithering) d *= 4; // No temporal dithering else // Do temporal dithering { d += ((f^y^(x&1)*2u ^ (x&2)/2u) & 3) << 6; // ^ This step is optional. It is a tradeoff: // Pros: it improves the spatial color resolution. // Cons: it causes flickering. The higher the framerate, // the less noticeable the flickering is. } _farnspokeb(0xA0000 + p, ColorConvert[0][(rgb >> 0) & 0xFF][d] + ColorConvert[1][(rgb >> 8) & 0xFF][d] + ColorConvert[2][(rgb >>16) & 0xFF][d]); } } void Close() // End graphics { textmode(C80); // set textmode again } } #include "math.tcc" #include "map.tcc" const unsigned nwalls = sizeof(map)/sizeof(*map); static bool TexturesInstalled = false; static GLuint WallTextureID; static bool UseAddmap[nwalls] = {false}; static unsigned LightmapIDs[nwalls] = { 0 }; static unsigned AddmapIDs[nwalls] = { 0 }; void InstallTexture(const void* data,int w,int h, int txno, int type1, int type2, int filter, int wrap) { glBindTexture(GL_TEXTURE_2D, txno); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); // Control how the texture repeats or not glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrap); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrap); // Control how the texture is rendered at different distances glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); // Decide upon the manner in which to import the texture if(filter == GL_LINEAR || filter == GL_NEAREST) glTexImage2D(GL_TEXTURE_2D, 0, type1, w,h, 0, type1, type2, data); else gluBuild2DMipmaps(GL_TEXTURE_2D, type1, w,h, type1, type2, data); } void ActivateTexture(int layer, int txno, int mode = GL_MODULATE) { glActiveTextureARB(layer); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, txno); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, mode); glColor3f(1,1,1); } void DisableTexture(int layer) { glActiveTextureARB(layer); glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); glColor3f(1,1,1); } // This function converts the level map into OpenGL quad primitives. // Not particularly optimized (in particular, everything is always rendered). static void ExtractLevelMap() { glShadeModel(GL_SMOOTH); // Walls are all created using this one texture. if(!TexturesInstalled) { // Generate a very simple rectangle of a texture. glGenTextures(1, &WallTextureID); glGenTextures(nwalls, LightmapIDs); glGenTextures(nwalls, AddmapIDs); const unsigned txW=256, txH=256; GLfloat texture[txH*txW]; for(unsigned y=0; y=txW || (y+8)>=txH)) * (0.1 + 0.3*std::pow((std::rand()%100)/100.0, 2.0)); ; InstallTexture(texture, txW,txH, WallTextureID, GL_LUMINANCE,GL_FLOAT, GL_LINEAR_MIPMAP_LINEAR, GL_REPEAT); } ActivateTexture(GL_TEXTURE0_ARB, WallTextureID); for(unsigned wallno=0; wallno < nwalls; ++wallno) { const maptype& m = map[wallno]; auto v10 = m.p[1] - m.p[0]; auto v30 = m.p[3] - m.p[0]; int width = v30.Len(); // Number of times the texture int height = v10.Len(); // is repeated across the surface. if(!TexturesInstalled) { // Load lightmap. unsigned lmW = width * 32, lmH = height * 32; std::vector map( lmW*lmH*3 ); char Buf[64]; std::sprintf(Buf, "lmap%u.raw", wallno); FILE* fp = std::fopen(Buf, "rb"); std::fread(&map[0], map.size(), sizeof(float), fp); std::fclose(fp); InstallTexture(&map[0],lmW,lmH, LightmapIDs[wallno], GL_RGB, GL_FLOAT, GL_LINEAR, GL_CLAMP_TO_EDGE); // Because OSMesa clamps all texture values into [0,1] range, meaning // that a lightsource can only darken the texture, never brighten it, // we must have a separate multiply-map and an add-map, where the // former can darken the texture and the latter can only brighten it. // (Unfortunately, due to how mathematics works, the add-map // is specific to the underlying texture is was designed for.) std::sprintf(Buf, "smap%u.raw", wallno); fp = std::fopen(Buf, "rb"); if(fp) { UseAddmap[wallno] = true; std::fread(&map[0], map.size(), sizeof(float), fp); std::fclose(fp); InstallTexture(&map[0],lmW,lmH, AddmapIDs[wallno], GL_RGB, GL_FLOAT, GL_LINEAR, GL_CLAMP_TO_EDGE); } } // TexturesInstalled if(UseAddmap[wallno]) ActivateTexture(GL_TEXTURE2_ARB, AddmapIDs[wallno], GL_ADD); else DisableTexture(GL_TEXTURE2_ARB); ActivateTexture(GL_TEXTURE1_ARB, LightmapIDs[wallno], GL_MODULATE); glNormal3fv( m.normal.d ); glBegin(GL_QUADS); for(unsigned e=0; e<4; ++e) { glMultiTexCoord2fARB(GL_TEXTURE0_ARB, width * !((e+2)&2), height * !((e+3)&2) ); glMultiTexCoord2fARB(GL_TEXTURE1_ARB, 1 * !((e+2)&2), 1 * !((e+3)&2) ); if(UseAddmap[wallno]) glMultiTexCoord2fARB(GL_TEXTURE2_ARB, 1 * !((e+2)&2), 1 * !((e+3)&2) ); glVertex3fv( m.p[e].d ); } glEnd(); } DisableTexture(GL_TEXTURE2_ARB); DisableTexture(GL_TEXTURE1_ARB); DisableTexture(GL_TEXTURE0_ARB); TexturesInstalled = true; } // These constants control vertical movement: const double gravity = -0.011, terminalvelocity = -2.0, jump = 0.18; class Actor { public: XYZ camera; // Where the actor is situated public: XYZ dir; // Where actor is looking (updated from look_angle, y=always zero) XYZ up; // What is the "up" direction for this actor public: Actor() : dir {{0,0,0}}, up {{0,-1,0}} { } virtual ~Actor() { } template void Render(Func& DrawWorld, double FoV, double aspect, double near = 1e-3) { // Decide upon how the viewport is to be projected. glMatrixMode(GL_PROJECTION); // Target matrix: Projection glLoadIdentity(); // Reset any transformations gluPerspective(FoV, aspect, near, 30.0); // Decide upon the manner in which the world is transformed from the // perspective of the viewport. In OpenGL, the camera never moves. // The world is simply rotated/scaled/shorn around the camera. glMatrixMode(GL_MODELVIEW); // Target matrix: World glLoadIdentity(); // Reset any transformations gluLookAt( camera.d[0], camera.d[1], camera.d[2], camera.d[0] + dir.d[0], camera.d[1] + dir.d[1], camera.d[2] + dir.d[2], up.d[0], up.d[1], up.d[2]); // Enable depth calculations to work on the new frame. glClear(GL_DEPTH_BUFFER_BIT); // Draw everything that should be rendered. DrawWorld(*this); // Tell OpenGL to render and display stuff. glFlush(); } }; class BlobActor: public Actor { public: double look_angle; // Angle of looking, around the Y axis double yaw; // Angle of aiming, in Y direction, visual effect only XYZ fatness; // Fatness of player avatar XYZ center; // Center of the player avatar XYZ fluctuation; public: bool moving; // Has nonzero velocity? bool ground; // Can jump? double move_angle; // Relative to looking-direction, the angle // in which the actor is trying to walk to XYZ vel; // Actor's current velocity int pushing; // -1 = decelerating, +1 = accelerating, 0 = idle public: BlobActor(): fatness {{1,1,1}}, center {{0,0,0}}, moving(true), ground(false), move_angle(0), vel {{0,0,0}}, pushing(0) { } virtual ~BlobActor() { } virtual void Update() { ground = true; if(pushing) { // Try to push into the looking-towards direction const double maxvel = 0.1*(pushing>0), acceleration = (pushing>0 ? 0.2 : 0.1); // Which direction we are actively trying to go XYZ move_vec = {{1,0,0}}; Matrix a; a.InitRotate( XYZ {{ 0,(look_angle+move_angle)*M_PI/180.0, 0 }} ); a.Transform(move_vec); // Update the current velocity so it slowly approaches // either the desired direction, or halt. Only update // the horizontal axis though; the vertical is handled // entirely by gravity. vel.d[0] = vel.d[0] * (1-acceleration) + move_vec.d[0] * (acceleration * maxvel); vel.d[2] = vel.d[2] * (1-acceleration) + move_vec.d[2] * (acceleration * maxvel); moving = true; } if(moving) { // For the purposes of collision testing, // the player is an axis-aligned ellipsoid. // We do collision tests in two phases. First horizontal, then // vertical. Attempting to do both at same time would make it // too difficult to decide reliably when the player can jump. ground = false; double yvel = std::max(vel.d[1] + gravity, terminalvelocity); vel.d[1] = 0.0; camera -= center; CollideAndSlide(camera, vel, fatness, map); if(CollideAndSlide(camera, {{0,yvel,0}}, fatness, map)) { if(yvel < 0) ground = true; yvel = 0.0; } vel.d[1] = yvel; camera += center; if(vel.Squared() < 1e-9) { vel *= 0.; pushing = 0; if(ground) moving = false; } } if(pushing) pushing = -1; double yaw_angle = yaw + vel.d[1] * 35.; dir = {{1, 0,0}}; up = {{0,-1,0}}; Matrix a; a.InitRotate( XYZ {{ 0,look_angle*M_PI/180.0, yaw_angle*M_PI/180.0 }} ); a.Transform(dir); a.Transform(up); } enum SignalType { sig_push, sig_jump, sig_aim }; void MovementSignal(SignalType type, int param1=0, int param2=0) { switch(type) { case sig_push: pushing = 1; move_angle = param1; break; case sig_jump: if(ground) { moving = true; vel.d[1] += jump; } break; case sig_aim: look_angle -= param1; yaw -= param2; if(yaw < -90) yaw = -90; if(yaw > 90) yaw = 90; // defer too aggressive tilts } } }; int main() { #if USE_OSMESA OSMesaContext om = OSMesaCreateContext(OSMESA_RGBA, NULL); OSMesaMakeCurrent(om, PC::ImageBuffer, GL_UNSIGNED_BYTE, PC::W, PC::H); #endif PC::Init(); //////// glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); glHint(GL_POLYGON_SMOOTH_HINT, GL_NICEST); {GLfloat v[4]={0,0,0,0}; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, v);} BlobActor player; player.fatness = {{ 0.2, 0.6, 0.2 }}; // Shape of the ellipsoid player.center = {{ 0, 0.3, 0 }}; // representing the actor player.camera = {{ 4, 3, 7.25 }}; // Location thereof player.look_angle = 170; player.yaw = 10;// Where it is facing auto RenderWorld = [&] (Actor& exclude_actor) { // Create white spheres representing all lightsources. DisableTexture(GL_TEXTURE0_ARB); DisableTexture(GL_TEXTURE1_ARB); DisableTexture(GL_TEXTURE2_ARB); for(const auto& l : lights) { glTranslated( l.pos.d[0], l.pos.d[1], l.pos.d[2] ); GLUquadric* qu = gluNewQuadric(); gluSphere(qu, 0.3f, 16, 16); gluDeleteQuadric(qu); glTranslated( -l.pos.d[0], -l.pos.d[1], -l.pos.d[2] ); } if(&exclude_actor != &player) { // For now, this blue sphere represents the player as well. glColor3f(.4,.4,.1); glPushMatrix(); glTranslated( player.camera.d[0], player.camera.d[1], player.camera.d[2] ); glTranslated( -player.center.d[0], -player.center.d[1], -player.center.d[2] ); glScaled(player.fatness.d[0], player.fatness.d[1], player.fatness.d[2]); GLUquadric* qu = gluNewQuadric(); gluSphere(qu, 1.0, 16, 16); gluDeleteQuadric(qu); glPopMatrix(); glColor3f(1,1,1); } ExtractLevelMap(); }; // Main loop for(;;) { const double fov = 90.0; player.Update(); player.Render(RenderWorld, fov, 4.0 / 3.0); // Render player's point of view #if USE_OSMESA PC::Render(); #else SDL_GL_SwapBuffers(); #endif while(kbhit()) switch(getch()) { case 'q': case 27: case 'Q': goto done; case 'w': player.MovementSignal(BlobActor::sig_push, 0); break; // forward case 's': player.MovementSignal(BlobActor::sig_push, 180); break; // backward case 'a': player.MovementSignal(BlobActor::sig_push, 90); break; // strafe left case 'd': player.MovementSignal(BlobActor::sig_push, -90); break; // strafe right case ' ': player.MovementSignal(BlobActor::sig_jump); break; // jump } // Get mouse relative position (since last poll) and update view __dpmi_regs regs = { }; regs.x.ax = 0xB; __dpmi_int(0x33, ®s); player.MovementSignal(BlobActor::sig_aim, (short) regs.x.cx, (short) regs.x.dx ); } done:; PC::Close(); #if USE_OSMESA OSMesaDestroyContext(om); #endif }