#include #include #include #include #include #include #include #include #include #include "3darith.h" #include "mathutils.h" #define WINX 400 #define WINY 400 #define MZX 3 #define MZY 3 extern const char *__progname; static Display *disp; static XVisualInfo *vi; static GLXContext ctx; static Screen *scr; static int scrwidth; static int scrheight; static int depth; static Window rootwin; static Colormap cmap; static Window win; static XColor bgcolour; static XColor fgcolour; static GC gc; static struct timeval nexttick; static unsigned int tex; static unsigned char texture[66][66][4]; /* * It would be conceptually easier to keep this state as the camera's * unit vectors (eg, forward, up, and right) as measured in world * coordinates. However, if we do that, we have to invert a * transformation matrix at rendering time. To make the render-time * operations easy, we have to instead maintain the converse: the * world basis vectors in eye coordinates. These are wx, wy, and wz: * the world x, y, and z unit vectors, in eye coordinates. However, * in order to make motion feasibly simple, we also maintain the * converse: the eye x, y, and z unit vectors in world coordinates. * * We could simply maintain them separately. But then numerical * roundoff errors would cause the two sets, theoretically inverses of * one another, to drift out of sync. So, instead, we depend on * another property: rotation and movement are usually well-separated, * meaning it is feasible to maintain only one of them and invert the * matrix when the other is needed. Since we constantly need wx/wy/wz * for rendering, it is ex/ey/ez that we maintain lazily. evalid is a * boolean indicating whether ex/ey/ez are valid. * * wx/wy/wz and ex/ey/ez represent only rotations. The player's * location in world coorindates is stored in ploc. Like wx/wy/wz, * this is always valid. * * "eye coordinates" are a coordinate system whose Z axis points * opposite the view direction, whose X axis points in the camera's * "right" direction, and whose Y axis points in the camera's "up" * direction. The Z axis is negated from the obvious convention * because that's required by the combination of two other useful * properties: (1) eye X and Y coordinates are "the same as" (parallel * to and same sign as) screen X and Y coordinates and (2) the eye * coordinate system obeys the same right-hand rule as the world * coordinate system. */ static XYZ wx; static XYZ wy; static XYZ wz; static XYZ ploc; static int evalid; static XYZ ex; static XYZ ey; static XYZ ez; static unsigned int kbstate; #define KBS_L 0x00000001 #define KBS_R 0x00000002 #define KBS_U 0x00000004 #define KBS_D 0x00000008 #define KBS_CW 0x00000010 #define KBS_CCW 0x00000020 #define KBS_F 0x00000040 #define KBS_B 0x00000080 #define KBS_LSHF 0x00000100 #define KBS_RSHF 0x00000200 #define KBS_LCTL 0x00000400 #define KBS_RCTL 0x00000800 #define KBS_SHIFT (KBS_LSHF|KBS_RSHF) #define KBS_CTRL (KBS_LCTL|KBS_RCTL) static int dbg; static int swapped; static void open_display(void) { disp = XOpenDisplay(0); if (disp == 0) { fprintf(stderr,"%s: can't open display\n",__progname); exit(1); } } static void find_visual(void) { XVisualInfo template; int nvi; template.visualid = 35; vi = XGetVisualInfo(disp,VisualIDMask,&template,&nvi); if (nvi < 1) { fprintf(stderr,"%s: can't find visual\n",__progname); exit(1); } if (nvi > 1) { fprintf(stderr,"%s: found multiple visuals\n",__progname); exit(1); } } static void setup_X(void) { Pixmap p; scr = XScreenOfDisplay(disp,vi->screen); scrwidth = XWidthOfScreen(scr); scrheight = XHeightOfScreen(scr); depth = vi->depth; rootwin = XRootWindowOfScreen(scr); cmap = XCreateColormap(disp,rootwin,vi->visual,AllocNone); XParseColor(disp,cmap,"#646",&bgcolour); XAllocColor(disp,cmap,&bgcolour); XParseColor(disp,cmap,"#fff",&fgcolour); XAllocColor(disp,cmap,&fgcolour); p = XCreatePixmap(disp,rootwin,1,1,depth); gc = XCreateGC(disp,p,0,0); XFreePixmap(disp,p); } static void setup_context(void) { ctx = glXCreateContext(disp,vi,0,True); if (! ctx) { fprintf(stderr,"%s: can't create GL context\n",__progname); exit(1); } } static int clip(int, int, int) __attribute__((__const__)); static int clip(int v, int min, int max) { return((vmax)?max:v); } static double blend(double, double, double) __attribute__((__const__)); static double blend(double v1, double a, double v2) { return((a*v2)+((1-a)*v1)); } static void setup_bitmaps(void) { int x; int y; double fx; double fy; for (y=66-1;y>=0;y--) { fy = (y - 1) / 64.0; for (x=66-1;x>=0;x--) { fx = ((x - 1) * 3) / 64.0; if ((x == 0) || (x == 66-1) || (y == 0) || (y == 66-1)) { texture[y][x][0] = 127; texture[y][x][1] = 127; texture[y][x][2] = 127; texture[y][x][3] = 255; } else { texture[y][x][0] = clip(blend(127.5,fy,(fx<1)?fx*256:0),0,255); texture[y][x][1] = clip(blend(127.5,fy,((fx>=1)&&(fx<2))?(fx-1)*256:0),0,255); texture[y][x][2] = clip(blend(127.5,fy,(fx>=2)?(fx-2)*256:0),0,255); texture[y][x][3] = 255; } } } } static void create_window(void) { XSetWindowAttributes attr; unsigned long int attrmask; attrmask = 0; attr.background_pixel = bgcolour.pixel; attrmask |= CWBackPixel; attr.event_mask = StructureNotifyMask | VisibilityChangeMask | KeyPressMask | KeyReleaseMask; attrmask |= CWEventMask; attr.colormap = cmap; attrmask |= CWColormap; win = XCreateWindow(disp,rootwin,0,0,WINX*2,WINY,0,depth,InputOutput,vi->visual,attrmask,&attr); XMapRaised(disp,win); } static void setup_gl(void) { double ps[4] = { .1, 0, 0, 0 }; double pt[4] = { 0, .1, 0, 0 }; if (glXMakeCurrent(disp,(GLXDrawable)win,ctx) != True) { fprintf(stderr,"%s: can't make context current\n",__progname); exit(1); } glShadeModel(GL_SMOOTH); glClearColor(0,0,0,0); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glFrustum(-.05,.05,-.05,.05,.1,5.1); glMatrixMode(GL_MODELVIEW); glEnable(GL_DEPTH_TEST); glEnable(GL_POLYGON_OFFSET_FILL); glPixelStorei(GL_UNPACK_ALIGNMENT,1); glGenTextures(1,&tex); glBindTexture(GL_TEXTURE_2D,tex); glTexEnvi(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_DECAL); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP); glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,66,66,1,GL_RGBA,GL_UNSIGNED_BYTE,&texture[0][0][0]); glTexGeni(GL_S,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR); glTexGeni(GL_T,GL_TEXTURE_GEN_MODE,GL_EYE_LINEAR); glTexGendv(GL_S,GL_EYE_PLANE,&ps[0]); glTexGendv(GL_T,GL_EYE_PLANE,&pt[0]); glCullFace(GL_BACK); glFrontFace(GL_CW); glEnable(GL_CULL_FACE); kbstate = 0; } static void setup_model(void) { } /* * The way GL transformations work: * * If the current matrix is C and the coordinates presented to (eg) * glVertex3d are V, then the transformed point P is * * P = C V * * or * * [ P(0) ] = [ C( 0) C( 4) C( 8) C(12) ] [ V(0) ] * [ P(1) ] = [ C( 1) C( 5) C( 9) C(13) ] [ V(1) ] * [ P(2) ] = [ C( 2) C( 6) C(10) C(14) ] [ V(2) ] * [ P(3) ] = [ C( 3) C( 7) C(11) C(15) ] [ V(3) ] * * Calling glMultMatrix(M) does C = C M, resulting in * * [ P(0) ] = [ C( 0) C( 4) C( 8) C(12) ] [ M( 0) M( 4) M( 8) M(12) ] [ V(0) ] * [ P(1) ] = [ C( 1) C( 5) C( 9) C(13) ] [ M( 1) M( 5) M( 9) M(13) ] [ V(1) ] * [ P(2) ] = [ C( 2) C( 6) C(10) C(14) ] [ M( 2) M( 6) M(10) M(14) ] [ V(2) ] * [ P(3) ] = [ C( 3) C( 7) C(11) C(15) ] [ M( 3) M( 7) M(11) M(15) ] [ V(3) ] * * Since we are transorming a world-coordinate V into an * eye-coordinate P, the values we need for M are wx/wy/wz. */ static void drawstuff(void) { GLdouble m[16]; if (dbg) { printf("\n"); printf("wx = (%g,%g,%g)\n",wx.x,wx.y,wx.z); printf("wy = (%g,%g,%g)\n",wy.x,wy.y,wy.z); printf("wz = (%g,%g,%g)\n",wz.x,wz.y,wz.z); printf("ploc = (%g,%g,%g)\n",ploc.x,ploc.y,ploc.z); if (evalid) { printf("ex = (%g,%g,%g)\n",ex.x,ex.y,ex.z); printf("ey = (%g,%g,%g)\n",ey.x,ey.y,ey.z); printf("ez = (%g,%g,%g)\n",ez.x,ez.y,ez.z); } dbg = 0; } m[0] = wx.x; m[1] = wx.y; m[2] = wx.z; m[3] = 0; m[4] = wy.x; m[5] = wy.y; m[6] = wy.z; m[7] = 0; m[8] = wz.x; m[9] = wz.y; m[10] = wz.z; m[11] = 0; m[12] = 0; m[13] = 0; m[14] = 0; m[15] = 1; glMultMatrixd(&m[0]); glTranslated(-ploc.x,-ploc.y,-ploc.z); glPolygonOffset(0,1); glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D,tex); glBegin(GL_QUADS); glColor3f(1,1,1); /* -y face */ glTexCoord2d(0,0); glVertex3d(-.5,-.5,-.5); glTexCoord2d(0,1); glVertex3d(-.5,-.5, .5); glTexCoord2d(1,1); glVertex3d( .5,-.5, .5); glTexCoord2d(1,0); glVertex3d( .5,-.5,-.5); /* +x face */ glTexCoord2d(0,0); glVertex3d( .5,-.5,-.5); glTexCoord2d(0,1); glVertex3d( .5,-.5, .5); glTexCoord2d(1,1); glVertex3d( .5, .5, .5); glTexCoord2d(1,0); glVertex3d( .5, .5,-.5); /* +y face */ glTexCoord2d(0,0); glVertex3d( .5, .5,-.5); glTexCoord2d(0,1); glVertex3d( .5, .5, .5); glTexCoord2d(1,1); glVertex3d(-.5, .5, .5); glTexCoord2d(1,0); glVertex3d(-.5, .5,-.5); /* -x face */ glTexCoord2d(0,0); glVertex3d(-.5, .5,-.5); glTexCoord2d(0,1); glVertex3d(-.5, .5, .5); glTexCoord2d(1,1); glVertex3d(-.5,-.5, .5); glTexCoord2d(1,0); glVertex3d(-.5,-.5,-.5); glEnd(); glDisable(GL_TEXTURE_2D); glBegin(GL_QUADS); /* -z face */ glColor3f(.333,.333,.333); glVertex3d(-.5,-.5,-.5); glVertex3d( .5,-.5,-.5); glVertex3d( .5, .5,-.5); glVertex3d(-.5, .5,-.5); /* +z face */ glColor3f(.667,.667,.667); glVertex3d(-.5,-.5, .5); glVertex3d(-.5, .5, .5); glVertex3d( .5, .5, .5); glVertex3d( .5,-.5, .5); glEnd(); } static void render(void) { glViewport(0,0,WINX*2,WINY); glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glLoadIdentity(); glViewport(swapped?WINX:0,0,WINX,WINY); glRotatef(-5,0,1,0); glPushMatrix(); drawstuff(); glPopMatrix(); glViewport(swapped?0:WINX,0,WINX,WINY); glRotatef(5,0,1,0); glPushMatrix(); drawstuff(); glPopMatrix(); glXSwapBuffers(disp,win); glXWaitGL(); glXWaitX(); } static void setup_ticks(void) { gettimeofday(&nexttick,0); } static void rotate_world(double a, XYZ axis) { wx = xyzunit(xyzrotate(wx,a,axis)); wy = xyzunit(subtract_component(xyzrotate(wy,a,axis),wx)); wz = xyzunit(subtract_component(subtract_component(xyzrotate(wz,a,axis),wx),wy)); evalid = 0; } /* * We basically have to invert the matrix * * [ rx.x ry.x rz.x ] * [ rx.y ry.y rz.y ] * [ rx.z ry.z rz.z ] * * Fortunately, because it's a rotation matrix, it's about as far from * singular as can be; its determinant is theoretically exactly 1. * * The closed-form inverse of * * [ A B C ] * [ D E F ] * [ G H I ] * * is * * [ EI-FH CH-BI BF-CE ] / * [ FG-DI AI-CG CD-AF ] / AEI+BFG+CDH-AFH-BDI-CEG * [ DH-EG BG-AH AE-BD ] / * * We save some multiplications by writing the determinant as * A(EI-FH)+B(FG-DI)+C(DH-EG), for 9 multiplies and 5 adds, versus the * above, which is 12 multiplies and 5 adds. */ static void invert_rotations(XYZ rx, XYZ ry, XYZ rz, XYZ *ix, XYZ *iy, XYZ *iz) { double det; det = (rx.x * ((ry.y * rz.z) - (rz.y * ry.z))) + (ry.x * ((rz.y * rx.z) - (rx.y * rz.z))) + (rz.x * ((rx.y * ry.z) - (ry.y * rx.z))); *ix = (XYZ) { ((ry.y * rz.z) - (rz.y * ry.z)) / det, ((rz.y * rx.z) - (rx.y * rz.z)) / det, ((rx.y * ry.z) - (ry.y * rx.z)) / det }; *iy = (XYZ) { ((rz.x * ry.z) - (ry.x * rz.z)) / det, ((rx.x * rz.z) - (rz.x * rx.z)) / det, ((ry.x * rx.z) - (rx.x * ry.z)) / det }; *iz = (XYZ) { ((ry.x * rz.y) - (rz.x * ry.y)) / det, ((rz.x * rx.y) - (rx.x * rz.y)) / det, ((rx.x * ry.y) - (ry.x * rx.y)) / det }; } static void ensure_e(void) { if (evalid) return; invert_rotations(wx,wy,wz,&ex,&ey,&ez); evalid = 1; } static void motion(void) { double f; if (kbstate & KBS_SHIFT) f = 15; else f = 3; f *= .3; switch (kbstate & (KBS_L|KBS_R)) { case KBS_L: if (kbstate & KBS_CTRL) { ensure_e(); ploc = xyzadd(ploc,xyzscale(ex,-f*.1)); } else { rotate_world(-f,(XYZ){0,1,0}); } break; case KBS_R: if (kbstate & KBS_CTRL) { ensure_e(); ploc = xyzadd(ploc,xyzscale(ex,f*.1)); } else { rotate_world(f,(XYZ){0,1,0}); } break; } switch (kbstate & (KBS_U|KBS_D)) { case KBS_U: if (kbstate & KBS_CTRL) { ensure_e(); ploc = xyzadd(ploc,xyzscale(ey,f*.1)); } else { rotate_world(-f,(XYZ){1,0,0}); } break; case KBS_D: if (kbstate & KBS_CTRL) { ensure_e(); ploc = xyzadd(ploc,xyzscale(ey,-f*.1)); } else { rotate_world(f,(XYZ){1,0,0}); } break; } switch (kbstate & (KBS_CW|KBS_CCW)) { case KBS_CW: rotate_world(-f,(XYZ){0,0,1}); break; case KBS_CCW: rotate_world(f,(XYZ){0,0,1}); break; } switch (kbstate & (KBS_F|KBS_B)) { case KBS_F: ensure_e(); ploc = xyzadd(ploc,xyzscale(ez,-f*.1)); break; case KBS_B: ensure_e(); ploc = xyzadd(ploc,xyzscale(ez,f*.1)); break; } } static void center_camera(void) { XYZ frommodel; XYZ axis; double len; double angle; ensure_e(); frommodel = xyzunit(ploc); axis = xyzcross(ez,frommodel); len = xyzlength(axis); if (len < 1e-6) { ez = frommodel; ey = xyzunit(subtract_component(ey,ez)); } else { axis = xyzunit(axis); angle = acosdeg(dot(ez,frommodel)); ez = xyzunit(xyzrotate(ez,angle,axis)); ey = xyzunit(subtract_component(xyzrotate(ey,angle,axis),ez)); } ex = xyzcross(ey,ez); invert_rotations(ex,ey,ez,&wx,&wy,&wz); evalid = 1; } static void keystroke(XKeyEvent *ev, char updn) { KeySym ks; unsigned int bit; ks = XLookupKeysym(ev,0); bit = 0; switch (ks) { case XK_h: case XK_H: bit = KBS_L; break; case XK_l: case XK_L: bit = KBS_R; break; case XK_k: case XK_K: bit = KBS_U; break; case XK_j: case XK_J: bit = KBS_D; break; case XK_w: case XK_W: bit = KBS_F; break; case XK_s: case XK_S: bit = KBS_B; break; case XK_y: case XK_Y: bit = KBS_CCW; break; case XK_u: case XK_U: bit = KBS_CW; break; case XK_Shift_L: bit = KBS_LSHF; break; case XK_Shift_R: bit = KBS_RSHF; break; case XK_Control_L: bit = KBS_LCTL; break; case XK_Control_R: bit = KBS_RCTL; break; case XK_x: case XK_X: if (updn == 'd') swapped = ! swapped; break; case XK_c: case XK_C: if (updn == 'd') center_camera(); break; case XK_d: case XK_D: if (updn == 'd') dbg = 1; break; case XK_q: case XK_Q: if ((updn == 'u') && (kbstate & KBS_SHIFT)) exit(0); break; } switch (updn) { case 'u': kbstate &= ~bit; break; case 'd': kbstate |= bit; break; default: abort(); break; } } static void events(void) { XEvent ev; while (XPending(disp)) { XNextEvent(disp,&ev); switch (ev.type) { default: /* XXX ignore */ break; case KeyPress: /* XKeyPressedEvent - XKeyEvent - xkey */ keystroke(&ev.xkey,'d'); break; case KeyRelease: /* XKeyReleasedEvent - XKeyEvent - xkey */ keystroke(&ev.xkey,'u'); break; case MappingNotify: /* XMappingEvent - xmapping */ XRefreshKeyboardMapping(&ev.xmapping); break; } } } static void await(void) { struct timeval now; int ms; nexttick.tv_usec += 75000; if (nexttick.tv_usec >= 1000000) { nexttick.tv_usec -= 1000000; nexttick.tv_sec ++; } while (1) { gettimeofday(&now,0); if ( (now.tv_sec > nexttick.tv_sec) || ( (now.tv_sec == nexttick.tv_sec) && (now.tv_usec >= nexttick.tv_usec) ) ) return; ms = ((nexttick.tv_sec - now.tv_sec) * 1000) + 1 + (nexttick.tv_usec / 1000) - (now.tv_usec / 1000); if (ms < 1) ms = 1; poll(0,0,ms); } } static void tick(void) { motion(); render(); events(); await(); } static void setup_view(void) { ploc = (XYZ){0,0,-2}; wx = (XYZ){0,1,0}; wy = (XYZ){1,0,0}; wz = (XYZ){0,0,-1}; evalid = 0; swapped = 0; } static void setup_input(void) { XGrabKeyboard(disp,win,False,GrabModeAsync,GrabModeAsync,CurrentTime); } int main(void); int main(void) { open_display(); find_visual(); setup_X(); setup_context(); setup_bitmaps(); create_window(); setup_gl(); setup_input(); setup_model(); setup_view(); setup_ticks(); while (1) tick(); }