// 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 #include // For everything OpenGL, but done all in software. #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 // DJGPP-specific include files, for accessing the screen & keyboard etc.: #include // For kbhit, getch, textmode (console access) #include // For outportb (palette access) #include // For __dpmi_int (mouse access) #include // For _dos_ds (VRAM access) #include // For _farsetsel and _farnspokeb (VRAM access) 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) 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 } } // Vector mathematics #define vdot(a,b) (a[0]*b[0] + a[1]*b[1] + a[2]*b[2]) #define vcross(a,b) {a[1]*b[2] - a[2]*b[1], a[2]*b[0] - a[0]*b[2], a[0]*b[1] - a[1]*b[0]} #define vsub(a,b) {a[0]-b[0], a[1]-b[1], a[2]-b[2]} #define vlen(a) std::sqrt(vdot(a,a)) #define vaddm(t,b,m) for(unsigned a=0; a<3; ++a) t[a] += b[a] * (m) // List of the walls that comprise the level map. In no particular order... // Walls are made of four corner vertices, and a "normal" which simply // tells where the wall is "looking" towards. It is used for reflecting // light correctly and for warding the player off. static const struct { GLfloat normal[3], p[4][3]; } map[] = { {{ 0, 0, 1}, {{ 6, 2,1},{ 6, 5,1},{ 1, 5,1},{ 1, 2,1}} }, // 0 {{ 0, 0, 1}, {{ 8, 4,1},{ 8, 5,1},{ 6, 5,1},{ 6, 4,1}} }, // 1 {{ 0, 0, 1}, {{24, 4,1},{24, 5,1},{16, 5,1},{16, 4,1}} }, // 2 {{ 0, 0, 1}, {{24, 3,1},{24, 4,1},{14, 4,1},{14, 3,1}} }, // 3 {{ 0, 0, 1}, {{24, 5,1},{24, 6,1},{17, 6,1},{17, 5,1}} }, // 4 {{ 0, 0, 1}, {{24, 6,1},{24, 9,1},{13, 9,1},{13, 6,1}} }, // 5 {{ 0,-1, 0}, {{24, 9,1},{24, 9,3},{16, 9,3},{16, 9,1}} }, // 6 {{ 0,-1, 0}, {{24, 9,4},{24, 9,7},{16, 9,7},{16, 9,4}} }, // 7 {{ 0,-1, 0}, {{24, 9,3},{24, 9,4},{17, 9,4},{17, 9,3}} }, // 8 {{ 0,-1, 0}, {{24, 9,7},{24, 9,8},{17, 9,8},{17, 9,7}} }, // 9 {{ 0,-1, 0}, {{16, 9,1},{16, 9,8},{13, 9,8},{13, 9,1}} }, // 10 {{ 0, 0,-1}, {{16, 9,8},{16, 8,8},{13, 8,8},{13, 9,8}} }, // 11 {{ 0, 0,-1}, {{16, 7,8},{16, 6,8},{13, 6,8},{13, 7,8}} }, // 12 {{ 0, 0,-1}, {{16, 8,8},{16, 7,8},{11, 7,8},{11, 8,8}} }, // 13 {{ 0, 0,-1}, {{16, 6,8},{16, 2,8},{ 1, 2,8},{ 1, 6,8}} }, // 14 {{ 0, 0,-1}, {{11, 4,5},{11, 2,5},{ 8, 2,5},{ 8, 4,5}} }, // 15 {{ 0,-1, 0}, {{11, 4,1},{11, 4,5},{ 8, 4,5},{ 8, 4,1}} }, // 16 {{ 0, 1, 0}, {{ 6, 4,1},{ 6, 4,2},{ 7, 4,2},{ 7, 4,1}} }, // 17 {{ 1, 0, 0}, {{ 7, 4,1},{ 7, 4,2},{ 7, 2,2},{ 7, 2,1}} }, // 18 {{ 0, 1, 0}, {{ 7, 2,1},{ 7, 2,2},{14, 2,2},{14, 2,1}} }, // 19 {{ 0, 1, 0}, {{ 8, 2,2},{ 8, 2,5},{14, 2,5},{14, 2,2}} }, // 20 {{ 0, 1, 0}, {{ 8, 2,6},{ 8, 2,8},{16, 2,8},{16, 2,6}} }, // 21 {{ 0, 1, 0}, {{ 1, 2,1},{ 1, 2,8},{ 6, 2,8},{ 6, 2,1}} }, // 22 {{ 1, 0, 0}, {{ 1, 5,1},{ 1, 5,8},{ 1, 2,8},{ 1, 2,1}} }, // 23 {{ 1, 0, 0}, {{ 1, 6,4},{ 1, 6,8},{ 1, 5,8},{ 1, 5,4}} }, // 24 {{ 0,-1, 0}, {{ 7, 6,4},{ 7, 6,8},{ 1, 6,8},{ 1, 6,4}} }, // 25 {{ 0,-1, 0}, {{ 8, 6,4},{ 8, 6,6},{ 7, 6,6},{ 7, 6,4}} }, // 26 {{ 0, 1, 0}, {{16, 6,4},{16, 6,7},{17, 6,7},{17, 6,4}} }, // 27 {{ 0, 1, 0}, {{16, 6,1},{16, 6,3},{17, 6,3},{17, 6,1}} }, // 28 {{ 1, 0, 0}, {{17, 6,1},{17, 6,3},{17, 5,3},{17, 5,1}} }, // 29 {{ 1, 0, 0}, {{17, 6,4},{17, 6,7},{17, 5,7},{17, 5,4}} }, // 30 {{ 0,-1, 0}, {{17, 5,4},{17, 5,7},{16, 5,7},{16, 5,4}} }, // 31 {{ 0,-1, 0}, {{17, 5,1},{17, 5,3},{16, 5,3},{16, 5,1}} }, // 32 {{ 0, 0,-1}, {{17, 5,3},{17, 2,3},{16, 2,3},{16, 5,3}} }, // 33 {{ 0, 0,-1}, {{17, 5,7},{17, 2,7},{16, 2,7},{16, 5,7}} }, // 34 {{ 0, 0,-1}, {{17, 9,7},{17, 6,7},{16, 6,7},{16, 9,7}} }, // 35 {{ 0, 0,-1}, {{17, 9,3},{17, 6,3},{16, 6,3},{16, 9,3}} }, // 36 {{ 0, 0, 1}, {{17, 2,4},{17, 5,4},{16, 5,4},{16, 2,4}} }, // 37 {{ 0, 0, 1}, {{17, 6,4},{17, 9,4},{16, 9,4},{16, 6,4}} }, // 38 {{ 0, 0, 1}, {{15, 2,4},{15, 3,4},{14, 3,4},{14, 2,4}} }, // 39 {{ 0, 1, 0}, {{15, 2,1},{15, 2,4},{16, 2,4},{16, 2,1}} }, // 40 {{ 1, 0, 0}, {{15, 3,1},{15, 3,4},{15, 2,4},{15, 2,1}} }, // 41 {{ 0, 1, 0}, {{14, 3,1},{14, 3,4},{15, 3,4},{15, 3,1}} }, // 42 {{-1, 0, 0}, {{14, 2,1},{14, 2,4},{14, 3,4},{14, 3,1}} }, // 43 {{-1, 0, 0}, {{16, 2,3},{16, 2,4},{16, 9,4},{16, 9,3}} }, // 44 {{-1, 0, 0}, {{16, 2,7},{16, 2,8},{16, 9,8},{16, 9,7}} }, // 45 {{-1, 0, 0}, {{16, 5,1},{16, 5,3},{16, 6,3},{16, 6,1}} }, // 46 {{-1, 0, 0}, {{16, 5,4},{16, 5,7},{16, 6,7},{16, 6,4}} }, // 47 {{-1, 0, 0}, {{ 8, 5,4},{ 8, 5,5},{ 8, 6,5},{ 8, 6,4}} }, // 48 {{-1, 0, 0}, {{ 8, 2,5},{ 8, 2,6},{ 8, 6,6},{ 8, 6,5}} }, // 49 {{-1, 0, 0}, {{ 8, 4,1},{ 8, 4,5},{ 8, 5,5},{ 8, 5,1}} }, // 50 {{ 0,-1, 0}, {{ 8, 5,1},{ 8, 5,4},{ 1, 5,4},{ 1, 5,1}} }, // 51 {{ 0, 0, 1}, {{ 8, 5,4},{ 8, 6,4},{ 1, 6,4},{ 1, 5,4}} }, // 52 {{ 0, 0, 1}, {{11, 2,6},{11, 6,6},{ 8, 6,6},{ 8, 2,6}} }, // 53 {{ 0, 0, 1}, {{11, 6,6},{11,18,6},{ 7,18,6},{ 7, 6,6}} }, // 54 {{ 0, 0, 1}, {{13, 7,6},{13, 8,6},{11, 8,6},{11, 7,6}} }, // 55 {{ 0,-1, 0}, {{13, 8,6},{13, 8,8},{11, 8,8},{11, 8,6}} }, // 56 {{ 0,-1, 0}, {{13, 6,1},{13, 6,8},{11, 6,8},{11, 6,1}} }, // 57 {{ 1, 0, 0}, {{13, 7,1},{13, 7,8},{13, 6,8},{13, 6,1}} }, // 58 {{ 1, 0, 0}, {{13, 9,1},{13, 9,6},{13, 7,6},{13, 7,1}} }, // 59 {{ 1, 0, 0}, {{13, 9,6},{13, 9,8},{13, 8,8},{13, 8,6}} }, // 60 {{ 1, 0, 0}, {{17, 9,7},{17, 9,8},{17, 4,8},{17, 4,7}} }, // 61 {{ 1, 0, 0}, {{17, 9,3},{17, 9,4},{17, 2,4},{17, 2,3}} }, // 62 {{ 0, 1, 0}, {{17, 2,3},{17, 2,4},{20, 2,4},{20, 2,3}} }, // 63 {{ 0, 1, 0}, {{11, 2,5},{11, 2,6},{20, 2,6},{20, 2,5}} }, // 64 {{ 0, 1, 0}, {{14, 2,4},{14, 2,5},{20, 2,5},{20, 2,4}} }, // 65 {{ 0, 1, 0}, {{16, 2,6},{16, 2,7},{20, 2,7},{20, 2,6}} }, // 66 {{ 0, 1, 0}, {{16, 2,1},{16, 2,3},{23, 2,3},{23, 2,1}} }, // 67 {{-1, 0, 0}, {{23, 2,1},{23, 2,3},{23, 3,3},{23, 3,1}} }, // 68 {{ 0, 1, 0}, {{23, 3,1},{23, 3,3},{24, 3,3},{24, 3,1}} }, // 69 {{ 0, 1, 0}, {{22, 3,3},{22, 3,6},{24, 3,6},{24, 3,3}} }, // 70 {{ 0, 1, 0}, {{20, 3,3},{20, 3,7},{22, 3,7},{22, 3,3}} }, // 71 {{-1, 0, 0}, {{20, 2,3},{20, 2,7},{20, 3,7},{20, 3,3}} }, // 72 {{-1, 0, 0}, {{20, 4,7},{20, 4,8},{20, 5,8},{20, 5,7}} }, // 73 {{ 0, 1, 0}, {{20, 5,7},{20, 5,8},{22, 5,8},{22, 5,7}} }, // 74 {{-1, 0, 0}, {{22, 5,7},{22, 5,8},{22, 7,8},{22, 7,7}} }, // 75 {{ 0, 1, 0}, {{22, 7,7},{22, 7,8},{24, 7,8},{24, 7,7}} }, // 76 {{-1, 0, 0}, {{24, 7,7},{24, 7,8},{24, 9,8},{24, 9,7}} }, // 77 {{-1, 0, 0}, {{24, 3,1},{24, 3,6},{24, 9,6},{24, 9,1}} }, // 78 {{-1, 0, 0}, {{24, 6,6},{24, 6,7},{24, 9,7},{24, 9,6}} }, // 79 {{ 0, 1, 0}, {{22, 6,6},{22, 6,7},{24, 6,7},{24, 6,6}} }, // 80 {{-1, 0, 0}, {{22, 3,6},{22, 3,7},{22, 6,7},{22, 6,6}} }, // 81 {{ 0, 0,-1}, {{24, 6,6},{24, 3,6},{22, 3,6},{22, 6,6}} }, // 82 {{ 0, 0,-1}, {{24, 9,8},{24, 7,8},{22, 7,8},{22, 9,8}} }, // 83 {{ 0, 0,-1}, {{22, 9,8},{22, 5,8},{20, 5,8},{20, 9,8}} }, // 84 {{ 0, 0,-1}, {{20, 9,8},{20, 4,8},{17, 4,8},{17, 9,8}} }, // 85 {{ 0, 0,-1}, {{20, 4,7},{20, 2,7},{17, 2,7},{17, 4,7}} }, // 86 {{ 0, 0,-1}, {{22, 5,7},{22, 3,7},{20, 3,7},{20, 5,7}} }, // 87 {{ 0, 0,-1}, {{24, 7,7},{24, 6,7},{22, 6,7},{22, 7,7}} }, // 88 {{ 0, 0,-1}, {{23, 3,3},{23, 2,3},{20, 2,3},{20, 3,3}} }, // 89 {{ 0, 0, 1}, {{23, 2,1},{23, 3,1},{15, 3,1},{15, 2,1}} }, // 90 {{ 0, 0, 1}, {{16, 4,1},{16, 6,1},{11, 6,1},{11, 4,1}} }, // 91 {{ 1, 0, 0}, {{11, 6,1},{11, 6,5},{11, 4,5},{11, 4,1}} }, // 92 {{ 1, 0, 0}, {{11, 6,5},{11, 6,6},{11, 2,6},{11, 2,5}} }, // 93 {{-1, 0, 0}, {{11, 6,6},{11, 6,8},{11, 7,8},{11, 7,6}} }, // 94 {{-1, 0, 0}, {{11, 8,6},{11, 8,8},{11,18,8},{11,18,6}} }, // 95 {{ 0,-1, 0}, {{11,18,6},{11,18,8},{ 7,18,8},{ 7,18,6}} }, // 96 {{ 0, 0,-1}, {{11,18,8},{11, 6,8},{ 7, 6,8},{ 7,18,8}} }, // 97 {{ 0, 0, 1}, {{14, 2,1},{14, 4,1},{ 7, 4,1},{ 7, 2,1}} }, // 98 {{ 0, 0, 1}, {{ 7, 2,2},{ 7, 4,2},{ 6, 4,2},{ 6, 2,2}} }, // 99 {{-1, 0, 0}, {{ 6, 2,1},{ 6, 2,2},{ 6, 4,2},{ 6, 4,1}} }, // 100 {{ 0, 1, 0}, {{ 6, 2,2},{ 6, 2,8},{ 8, 2,8},{ 8, 2,2}} }, // 101 {{ 0, 1, 0}, {{11, 7,6},{11, 7,8},{13, 7,8},{13, 7,6}} }, // 102 {{ 0, 1, 0}, {{17, 4,7},{17, 4,8},{20, 4,8},{20, 4,7}} }, // 103 {{ 1, 0, 0}, {{ 7,18,6},{ 7,18,8},{ 7, 6,8},{ 7, 6,6}} }, // 104 }; // Light sources. All of them are simply 3D points with a color. static const struct { float pos[3], dif[3]; } lights[] = { { { 15.2 , 2.2, 1.5 }, { .1, 0.6, 1 } }, // orange on the floor { { 17.3 , 5.7, 7.5 }, { 1,.2, .2 } }, // blue at the end { { 9.5 , 17.5, 7 }, { 200,200,200 } }, // huge white in the sky tunnel { { 9.5 , 3.9, 1.1 }, {.2, .4, 2 } } // blue in tunnel }; struct HitRec { unsigned wallno; float distance, hit[3], alpha, beta; HitRec() : wallno(~0u), distance(0), hit() { } bool set() const { return wallno != ~0u; } }; HitRec IntersectRay(const double org[3], const double dir[3]) { // Ray-quadrilateral intersection test by Ares Lagae and Philip Dutré. // Because our walls are simple rectangles, the calculation of // alpha11 and beta11 (both of which are always 1.0) was skipped, // and many extra branches were eliminated, simpifying the code. // Our walls are also perpendicular to main axis, but it is a coincidence. HitRec result; for(unsigned wallno=0; wallno < sizeof(map)/sizeof(*map); ++wallno) { const auto& q = map[wallno]; const double eps = 1e-9; #define halftest(A,B,C) \ double e##A##B[3] = vsub(q.p[B], q.p[A]); \ double e##A##C[3] = vsub(q.p[C], q.p[A]); \ double vp[3] = vcross(dir, e##A##C); \ double det = vdot(e##A##B, vp); \ if(std::abs(det) < eps) continue; \ double vt[3] = vsub(org, q.p[A]); \ double inv_det = 1. / det; \ double alpha = vdot(vt, vp) * inv_det; \ if(alpha < 0.f) continue; \ double vq[3] = vcross(vt, e##A##B); \ double beta = vdot(dir, vq) * inv_det; \ if(beta < 0.f) continue halftest(0,1,3); if( (alpha + beta) > 1.) { halftest(2,3,1); } // Compute the ray parameter of the intersection point, and // reject the ray if it does not hit q. Choose nearest hit. double t = vdot(e03, vq) * inv_det; if(t >= 0. && (!result.set() || t < result.distance)) { result.wallno = wallno; result.distance = t; result.alpha = alpha; result.beta = beta; } } if(result.set()) for(unsigned a=0; a<3; ++a) result.hit[a] = org[a] + dir[a] * result.distance; return result; } static 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, filter); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter); // Control how the texture is mathematically applied glTexEnvi (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); // 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); } // This function converts the level map into OpenGL quad primitives. // Not particularly optimized (in particular, everything is always rendered). static void ExtractLevelMap() { // Walls are all created using this one texture. glActiveTextureARB(GL_TEXTURE0_ARB); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, 1u); glColor3f(1,1,1); glActiveTextureARB(GL_TEXTURE1_ARB); glEnable(GL_TEXTURE_2D); glShadeModel(GL_SMOOTH); for(unsigned wallno=0; wallno < sizeof(map)/sizeof(*map); ++wallno) { const auto& m = map[wallno]; float v10[3] = vsub(m.p[1], m.p[0]); // Vector for wall's one size float v30[3] = vsub(m.p[3], m.p[0]); // and the other int width = vlen(v30); // Number of times the texture int height = vlen(v10); // is repeated across the surface. // Create a diffuse + radiosity lightmap. unsigned lmW = width * 32, lmH = height * 32; std::vector map( lmW*lmH*3 ); // Load it from a file I have created beforehand. The code to generate // the lightmap adds about 200 lines to this program's size, and there // isn't enough time in Youtube's limit for me to include that part here. // Simply put, it calls IntersectRay many times to see which lightsources // are visible from each spot, and then it does the same repeatedly to // many random directions to catch indirect light ("global illumination"). 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, 2+wallno, GL_RGB, GL_FLOAT, GL_LINEAR, GL_CLAMP_TO_EDGE); glNormal3fv( m.normal ); 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) ); glVertex3fv( m.p[e] ); } glEnd(); } } int main() { OSMesaContext om = OSMesaCreateContext(OSMESA_RGBA, NULL); OSMesaMakeCurrent(om, PC::ImageBuffer, GL_UNSIGNED_BYTE, PC::W, PC::H); //////// glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); glCullFace(GL_BACK); // Generate a very simple rectangle of a texture. const unsigned txW=256, txH=256; GLfloat texture[txH*txW]; for(unsigned y=0; y=txW || (y+8)>=txH)) ; InstallTexture(texture, txW,txH, 1u, GL_LUMINANCE,GL_FLOAT, GL_LINEAR_MIPMAP_LINEAR, GL_REPEAT); PC::Init(); bool need_redraw = true; const double below = 1.0, above = 0.3, around = 0.2; double look_angle = 170, yaw = 10, move_angle = 0; double camera[3] = { 4, 3, 7.25 }; // Current co-ordinates of our camera double dir[3] = { 1, 0, 0 }; // Current look-towards vector double hvel[3] = {0,0,0}, vvel = 0; bool falling = true; int moving = 0; // Main loop for(;;) { if(need_redraw) { dir[0] = std::sin(look_angle * M_PI / 180.0); dir[2] = std::cos(look_angle * M_PI / 180.0); // Decide upon how the viewport is to be projected. glMatrixMode(GL_PROJECTION); // Target matrix: Projection glLoadIdentity(); // Reset any transformations glFrustum(-.1333e-2, .1333e-2, -.1e-2, .1e-2, .001, 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 glRotatef(yaw+vvel*35.f, 1,0,0); // autotilt camera when jumping #define drand(m) ((std::rand()%1000-500)*5e-2*(m)) if(moving) // add some camera shake glRotatef( drand(.1) * (vlen(hvel) + std::abs(vvel)), 0,0,1); gluLookAt( camera[0], camera[1], camera[2], camera[0] + dir[0] + drand(6e-5), camera[1] + dir[1] + drand(6e-5), camera[2] + dir[2] + drand(6e-5), 0,-1,0); // Enable depth calculations to work on the new frame. glClear(GL_DEPTH_BUFFER_BIT); // Draw everything that should be rendered. // Create white spheres representing all lightsources. glActiveTextureARB(GL_TEXTURE1_ARB); glDisable(GL_TEXTURE_2D); glColor3f(1, 1, 1); glActiveTextureARB(GL_TEXTURE0_ARB); glDisable(GL_TEXTURE_2D); glColor3f(1, 1, 1); for(const auto& l : lights) { glTranslatef( l.pos[0], l.pos[1], l.pos[2] ); GLUquadric* qu = gluNewQuadric(); gluSphere(qu, 0.2f, 16, 16); gluDeleteQuadric(qu); glTranslatef( -l.pos[0], -l.pos[1], -l.pos[2] ); } ExtractLevelMap(); // Tell OpenGL to render and display stuff. glFlush(); need_redraw = false; } PC::Render(); // We do collision tests in two phases. For the purposes of // collision testing, the player is shaped like a letter "E". // We first check vertically, whether the player's top or bottom // (depending on vertical direction) is colliding with a wall. // Then we check horizontally, whether the nearest wall is // within the player's horizontal reach. // There exist better collision algorithms, but they are // also more difficult to implement. This is actually my // first OpenGL program, ever. Honestly. So bear with me. // I'm not used to doing vector mathematics. // Vertical collision detection bool ground = !falling; if(falling) { vvel -= 0.011; // Add gravity double down[3] = {0, vvel < 0 ? -1. : 1., 0}; HitRec vcollision = IntersectRay(camera, down); if(!vcollision.set()) { // Absolutely nothing is ahead in our current trajectory. // Stop falling, to prevent descending to infinity. falling = vvel > 0.f; vvel = 0; } else if(vvel < 0 && vcollision.distance <= below) { // The nearest wall is within our feet's distance. // Fix to ground. // TODO: land on feet sfx if vvel < -0.03 double fix = below - vcollision.distance; camera[1] += fix; vvel = 0; falling = false; need_redraw = true; ground = true; } else if(vvel > 0 && vcollision.distance <= above) { // The nearest wall is within our head's distance. // I.e. we hit a ceiling. vvel = 0; falling = true; } if(falling) { camera[1] += vvel; need_redraw = true; } } // Horizontal collision detection if(moving) { // Try to push into the desired direction const double maxvel = 0.1*(moving>0), acceleration = (moving>0 ? 0.2 : 0.1); double move_vec[3] = { dir[0] * std::cos(move_angle) - dir[2]*std::sin(move_angle), dir[1], dir[0] * std::sin(move_angle) + dir[2]*std::cos(move_angle) }; for(unsigned a=0; a<3; ++a) hvel[a] *= (1-acceleration); for(unsigned a=0; a<3; ++a) hvel[a] += move_vec[a]*acceleration*maxvel; double vel = vlen(hvel); if(std::abs(vel) < 1e-9) { // Stop moving if our velocity is low enough. moving = 0; } else { // TODO: walk sfx // Do two collision checks. // One for our feet level and one for our head level. double heights[2] = { -below+1e-4, above-1e-4 }; double movedir[3] = { hvel[0]/vel, hvel[1]/vel, hvel[2]/vel }; double wall_distance = 1e5f, oldvel = vel; unsigned nearest_wall = ~0u; for(unsigned h=0; h<2; ++h) { // Vantage point = camera + (feet or head level) double vantage[3] = { camera[0], camera[1] + heights[h], camera[2] }; HitRec hcollision = IntersectRay(vantage, movedir); if(hcollision.set() && hcollision.distance < wall_distance) { // There's a wall ahead us. Keep the nearest wall. wall_distance = hcollision.distance; nearest_wall = hcollision.wallno; } } // Figure out where the wall is in relation to our body part if(wall_distance < around) // already inside the wall, push out { const auto& normal = map[nearest_wall].normal; vaddm(camera, normal, around-wall_distance); vel = 0; moving = 0; } else if(wall_distance < vel) // would go inside wall if we moved now { // TODO: Move tangentially to the wall vel = wall_distance+1e-3f; moving = 0; } // Update the camera position according to our velocity vector. for(unsigned a=0; a<3; ++a) camera[a] += (hvel[a] *= vel / oldvel); need_redraw = true; falling = true; } } moving = -1; while(kbhit()) switch(getch()) { case 'q': case 27: case 'Q': goto done; case 'w': moving = 1; move_angle = 0; break; // forward case 's': moving = 1; move_angle = M_PI; break; // backward case 'a': moving = 1; move_angle = M_PI* .5; break; // strafe left case 'd': moving = 1; move_angle = M_PI*-.5; break; // strafe right case ' ': // jump if(ground) { vvel += 0.18; falling = true; // TODO: jump sfx } break; } // Get mouse relative position (since last poll) and update view __dpmi_regs regs = { }; regs.x.ax = 0xB; __dpmi_int(0x33, ®s); if(regs.x.cx || regs.x.dx) need_redraw = true; look_angle += (short) regs.x.cx; yaw += (short) regs.x.dx; if(yaw < -90) yaw = -90; if(yaw > 90) yaw = 90; // defer too aggressive tilts } done:; PC::Close(); OSMesaDestroyContext(om); }