// // Demonstrate Potential Visible Set (PVS) and Raycasting // for determining visibility of a simple world made of // grid squares // // by Jarno van der Linden jvan006@cs.auckland.ac.nz // #include #include #include #include #include #include #include #include typedef short Coord; const Coord WORLD_SIZE = 256; // Grid size of the world const Coord MAX_VISIBLE_DISTANCE = 64; // Viewing range const int NUM_RAYS = MAX_VISIBLE_DISTANCE * 8; // Number of raycasting rays const int RAYS_PER_SECTOR = NUM_RAYS / 4; // Number of rayscasting rays per sector // Position of a grid square struct GridPos { Coord x, y; }; // The PVS is simply an array of positions of visible grid squares typedef std::vector PVS; // Information about a grid square struct Square { bool wall; // True if this square is a wall PVS pvs; // PVS of all squares visible from this square }; // Ray depths for all rays are stored as squared distances // between grid squares. As the grid square positions are integers, // so is this distance typedef int RayDepthMap[NUM_RAYS]; // The world is a 2D grid of squares Square world[WORLD_SIZE][WORLD_SIZE]; // The player position GridPos player; enum { RENDER_FULLWORLD = 1 }; int rendermode = RENDER_FULLWORLD; // // Create a random world // Walls are placed by recursivly subdividing with alternating // horizontal and vertical walls. // Walls can only be placed if there is a wall at both ends // and there is enough space for a corridor. // A door is placed in the wall. // The algorithm guarantees that every room is reachable from // every other room by exactly one path. // const int CORRIDOR_SIZE = 8; // Half the smallest width of a corridor void PlaceVerticalWall(const GridPos& minp, const GridPos& maxp); // Place a horizontal wall in box [minp, maxp] void PlaceHorizontalWall(const GridPos& minp, const GridPos& maxp) { // Try finding a place for a horizontal wall that joins // a wall at both ends for(int t = 0; t < 8; t++) { // Pick random number in the range [miny+CORRIDOR_SIZE, maxy-CORRIDOR_SIZE] const Coord y = rand() % (maxp.y - minp.y - 2*CORRIDOR_SIZE + 1) + minp.y + CORRIDOR_SIZE; if(world[minp.x-1][y].wall && world[maxp.x+1][y].wall) { // Place a wall here for(int w = minp.x; w <= maxp.x; w++) world[w][y].wall = true; // Make a doorway of half the corridor width const Coord x = rand() % (maxp.x - minp.x - CORRIDOR_SIZE + 1) + minp.x; for(int w = x; w < x + CORRIDOR_SIZE; w++) world[w][y].wall = false; // If there is enough space, place a horizontal wall above if((y - minp.y) > 2*CORRIDOR_SIZE) { const GridPos topminp = { minp.x, minp.y }; const GridPos topmaxp = { maxp.x, y-1 }; PlaceVerticalWall(topminp, topmaxp); } // If there is enough space, place a horizontal wall below if((maxp.y - y) > 2*CORRIDOR_SIZE) { const GridPos bottomminp = { minp.x, y+1 }; const GridPos bottommaxp = { maxp.x, maxp.y }; PlaceVerticalWall(bottomminp, bottommaxp); } return; } } } // Place a vertical wall in box [minp, maxp] void PlaceVerticalWall(const GridPos& minp, const GridPos& maxp) { // Try finding a place for a vertical wall that joins // a wall at both ends for(int t = 0; t < 8; t++) { // Pick random number in the range [miny+CORRIDOR_SIZE, maxy-CORRIDOR_SIZE] const Coord x = rand() % (maxp.x - minp.x - 2*CORRIDOR_SIZE + 1) + minp.x + CORRIDOR_SIZE; if(world[x][minp.y-1].wall && world[x][maxp.y+1].wall) { // Place a wall here for(int w = minp.y; w <= maxp.y; w++) world[x][w].wall = true; // Make a doorway of half the corridor width const Coord y = rand() % (maxp.y - minp.y - CORRIDOR_SIZE + 1) + minp.y; for(int w = y; w < y + CORRIDOR_SIZE; w++) world[x][w].wall = false; // If there is enough space, place a horizontal wall on the left if((x - minp.x) > 2*CORRIDOR_SIZE) { const GridPos leftminp = { minp.x, minp.y }; const GridPos leftmaxp = { x-1, maxp.y }; PlaceHorizontalWall(leftminp, leftmaxp); } // If there is enough space, place a horizontal wall on the right if((maxp.x - x) > 2*CORRIDOR_SIZE) { const GridPos rightminp = { x+1, minp.y }; const GridPos rightmaxp = { maxp.x, maxp.y }; PlaceHorizontalWall(rightminp, rightmaxp); } return; } } } void InitWorld(void) { // Don't use the standard C library rand() random number generator // in a real game. It's almost always slow and very non-random. // But this is just example code, so we don't care. srand(time(NULL)); for(Coord x = 0; x < WORLD_SIZE; x++) { for(Coord y = 0; y < WORLD_SIZE; y++) { world[x][y].wall = false; } } // Place a wall around the world for(Coord x = 0; x < WORLD_SIZE; x++) world[x][0].wall = world[x][WORLD_SIZE-1].wall = true; for(Coord y = 0; y < WORLD_SIZE; y++) world[0][y].wall = world[WORLD_SIZE-1][y].wall = true; const GridPos minp = { 1, 1 }; const GridPos maxp = { WORLD_SIZE-2, WORLD_SIZE-2 }; if(rand() > RAND_MAX/2) PlaceHorizontalWall(minp, maxp); else PlaceVerticalWall(minp, maxp); // Place the player somewhere (not in a wall) do { player.x = rand() % WORLD_SIZE; player.y = rand() % WORLD_SIZE; } while(world[player.x][player.y].wall); } // // Raycasting and PVS // // Do a raycast from a viewing position in some direction. // Returns true if a wall was hit, false if nothing was hit // by the time the end of the world or visible range was reached. // If returning true, the position of the square hit is set. // r is the coordinate on the outer ring of the given sector // that varies. So for sectors 0 and 2 it is x, for sector 1 // and 3 it is y. // A function template is used to create optimized versions // specific for each sector at compile time. template bool Raycast(const GridPos& vp, const int r, GridPos& p) { for(Coord s = 1; s <= MAX_VISIBLE_DISTANCE; s++) { if(sector == 0) { p.x = vp.x + r * s / MAX_VISIBLE_DISTANCE; p.y = vp.y + s; } else if(sector == 1) { p.x = vp.x + s; p.y = vp.y + r * s / MAX_VISIBLE_DISTANCE; } else if(sector == 2) { p.x = vp.x + r * s / MAX_VISIBLE_DISTANCE; p.y = vp.y - s; } else if(sector == 3) { p.x = vp.x - s; p.y = vp.y + r * s / MAX_VISIBLE_DISTANCE; } // If the ray falls off the end of the world, return nothing hit if((p.x < 0) || (p.x >= WORLD_SIZE) || (p.y < 0) || (p.y >= WORLD_SIZE)) return false; // If the square is a wall, return a hit if(world[p.x][p.y].wall) return true; } // Gone beyond the visible range, return no hit return false; } // Compute a distance between two grid positions inline int Distance(const GridPos& vp, const GridPos& p) { // L2 distance return (p.x - vp.x) * (p.x - vp.x) + (p.y - vp.y) * (p.y - vp.y); // Manhatten distance //return abs(p.x - vp.x) + abs(p.y - vp.y); } // Compute the ray depth for all rays from a viewing given position void ComputeRayDepthMap(const GridPos& vp, RayDepthMap& raydepthmap) { // Go around the outer ring in clockwise direction int ray; GridPos p; // Sector 0 for(ray = 0; ray < RAYS_PER_SECTOR; ray++) { if(Raycast<0>(vp, ray - RAYS_PER_SECTOR/2, p)) raydepthmap[ray] = Distance(vp, p); else raydepthmap[ray] = INT_MAX; } // Sector 1 for(; ray < 2 * RAYS_PER_SECTOR; ray++) { if(Raycast<1>(vp, 3 * RAYS_PER_SECTOR/2 - ray, p)) raydepthmap[ray] = Distance(vp, p); else raydepthmap[ray] = INT_MAX; } // Sector 2 for(; ray < 3 * RAYS_PER_SECTOR; ray++) { if(Raycast<2>(vp, 5 * RAYS_PER_SECTOR/2 - ray, p)) raydepthmap[ray] = Distance(vp, p); else raydepthmap[ray] = INT_MAX; } // Sector 3 for(; ray < 4 * RAYS_PER_SECTOR; ray++) { if(Raycast<3>(vp, ray - 7 * RAYS_PER_SECTOR/2, p)) raydepthmap[ray] = Distance(vp, p); else raydepthmap[ray] = INT_MAX; } } // Figure out if a given square is visible from a viewing position // using a previously computed ray depth map bool IsSquareVisible(const GridPos& vp, const GridPos& p, const RayDepthMap& raydepthmap) { const Coord dpx = p.x - vp.x; const Coord dpy = p.y - vp.y; // Any squares beyond range are invisible if((abs(dpx) > MAX_VISIBLE_DISTANCE) || (abs(dpy) > MAX_VISIBLE_DISTANCE)) return false; // A square is always visible from itself if((dpx == 0) && (dpy == 0)) return true; // // Find a ray going through the square // // Compute ray number from direction. // This is all using integer math. There is probably some rounding errors, // but the end result looks okay. int ray; if((dpx >= -dpy) && (dpx < dpy)) // Sector 0 ray = ((dpx * RAYS_PER_SECTOR) / dpy + RAYS_PER_SECTOR) / 2; else if((dpx >= dpy) && (dpx > -dpy)) // Sector 1 ray = RAYS_PER_SECTOR + (RAYS_PER_SECTOR - (dpy * RAYS_PER_SECTOR) / dpx) / 2; else if((dpx <= -dpy) && (dpx > dpy)) // Sector 2 ray = 2 * RAYS_PER_SECTOR + ((dpx * RAYS_PER_SECTOR) / dpy + RAYS_PER_SECTOR) / 2; else // Sector 3 ray = 3 * RAYS_PER_SECTOR + (RAYS_PER_SECTOR - (dpy * RAYS_PER_SECTOR) / dpx) / 2; // Check distance to square against ray depth return Distance(vp, p) <= raydepthmap[ray]; } // Compute the PVS for a given viewing position void ComputePVSSquare(const GridPos& vp) { Square& square = world[vp.x][vp.y]; static std::bitset visiblesquare; square.pvs.clear(); visiblesquare.reset(); // Compute the ray depth for all rays from this position static RayDepthMap raydepthmap; ComputeRayDepthMap(vp, raydepthmap); // Go through all squares in the visible range and find which // are visible from the viewing position GridPos p; const Coord minx = max(vp.x - MAX_VISIBLE_DISTANCE, 1); const Coord miny = max(vp.y - MAX_VISIBLE_DISTANCE, 1); const Coord maxx = min(vp.x + MAX_VISIBLE_DISTANCE + 1, WORLD_SIZE-1); const Coord maxy = min(vp.y + MAX_VISIBLE_DISTANCE + 1, WORLD_SIZE-1); for(p.x = minx; p.x < maxx; p.x++) { for(p.y = miny; p.y < maxy; p.y++) { if(IsSquareVisible(vp, p, raydepthmap)) { square.pvs.push_back(p); visiblesquare[p.x * WORLD_SIZE + p.y] = true; } } } // If a square is visible, consider the neighbours as // potentially visible as well const int numpvs = (int)square.pvs.size(); for(int i = 0; i < numpvs; i++) { p = square.pvs[i]; const int offsets[4][2] = { {0, -1}, {-1, 0}, {1, 0}, {0, 1} }; for(int o = 0; o < 4; o++) { const GridPos q = { p.x + offsets[o][0], p.y + offsets[o][1] }; const int qidx = q.x * WORLD_SIZE + q.y; if(!visiblesquare[qidx]) { square.pvs.push_back(q); visiblesquare[qidx] = true; } } } } // Compute the PVS for all squares in the world // This may take a few minutes void ComputePVS(void) { GridPos p; for(p.x = 0; p.x < WORLD_SIZE; p.x++) { for(p.y = 0; p.y < WORLD_SIZE; p.y++) { ComputePVSSquare(p); } } } // // GLUT rendering and stuff // void Display(void) { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); if(rendermode & RENDER_FULLWORLD) { glBegin(GL_QUADS); for(Coord x = 0; x < WORLD_SIZE; x++) { for(Coord y = 0; y < WORLD_SIZE; y++) { if(world[x][y].wall) glColor3f(0.35f, 0.05f, 0.05f); else glColor3f(0, 0, 0); glVertex2f(x, y); glVertex2f(x+1, y); glVertex2f(x+1, y+1); glVertex2f(x, y+1); } } glEnd(); } const Square& playersquare = world[player.x][player.y]; glBegin(GL_QUADS); for(PVS::const_iterator vi = playersquare.pvs.begin(); vi != playersquare.pvs.end(); vi++) { const int dx = vi->x - player.x; const int dy = vi->y - player.y; float shade = 1 - (float)(dx*dx + dy*dy) / (MAX_VISIBLE_DISTANCE * MAX_VISIBLE_DISTANCE); if(shade < 0) shade = 0; if(world[vi->x][vi->y].wall) glColor3f(0.35f * (1 + shade), 0.05f * (1 + shade), 0.05f * (1 + shade)); else glColor3f(0.4f * shade, 0.4f * shade, 0.4f * shade); glVertex2f(vi->x, vi->y); glVertex2f(vi->x+1, vi->y); glVertex2f(vi->x+1, vi->y+1); glVertex2f(vi->x, vi->y+1); } glColor3f(0.1f, 0.7f, 0.1f); glVertex2f(player.x, player.y); glVertex2f(player.x+1, player.y); glVertex2f(player.x+1, player.y+1); glVertex2f(player.x, player.y+1); glEnd(); glutSwapBuffers(); } void Reshape(int w, int h) { glViewport(0, 0, w, h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0, WORLD_SIZE, 0, WORLD_SIZE, -1, 1); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); } void MovePlayer(int dx, int dy) { GridPos p = player; p.x += dx; p.y += dy; if((p.x >= 0) && (p.x < WORLD_SIZE) && (p.y >= 0) && (p.y < WORLD_SIZE) && !world[p.x][p.y].wall) { player = p; ComputePVSSquare(player); } } void Key(unsigned char key, int x, int y) { switch(key) { case 27: case 'q': exit(EXIT_SUCCESS); case 'a': MovePlayer(-1,0); break; case 'd': MovePlayer(1,0); break; case 'w': MovePlayer(0,1); break; case 's': MovePlayer(0,-1); break; case ' ': rendermode ^= RENDER_FULLWORLD; break; default: return; } glutPostRedisplay(); } // // Little main // int main(int argc, char *argv[]) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH); glutInitWindowSize(512, 512); glutCreateWindow("PVS Demo"); glutReshapeFunc(Reshape); glutKeyboardFunc(Key); glutDisplayFunc(Display); glDisable(GL_DEPTH_TEST); InitWorld(); MovePlayer(0, 0); glutMainLoop(); return EXIT_SUCCESS; }