// Alexandri Zavodny // CSE 40166: Computer Graphics, Fall 2010 // Example: Simple texturing example: use 't' to enable textures and rotate // using an arcball camera model. #include #include #include #include #include #include #include #include #include #include using namespace std; // GLOBAL VARIABLES //////////////////////////////////////////////////////////// static size_t windowWidth = 640; static size_t windowHeight = 480; static float aspectRatio; GLint leftMouseButton, rightMouseButton; //status of the mouse buttons int mouseX = 0, mouseY = 0; //last known X and Y of the mouse float cameraTheta, cameraPhi; //camera direction in spherical coordinates float cameraX, cameraY, cameraZ; //camera position in cartesian coordinates float dirX, dirY, dirZ; //camera direction in cartesian coordinates //some global variables for our textures unsigned char *grassTexData, *grassMaskData; //the actual image data GLuint grassTexHandle; //a handle to the texture in our OpenGL context int grassTexWidth, grassTexHeight; //variables for the image width/height //contains the X and Z coordinates of grass sprite positions. //is filled randomly at the start of execution, and then referenced //during rendering. vector< pair > grassSpritePositions; char *vertexShaderString; char *fragmentShaderString; GLuint vertexShaderHandle, fragmentShaderHandle, shaderProgramHandle; GLuint timeLocation; // readPPM() /////////////////////////////////////////////////////////////////// // // This function reads an ASCII PPM, returning true if the function succeeds and // false if it fails. If it succeeds, the variables imageWidth and // imageHeight will hold the width and height of the read image, respectively. // // It's not terribly robust. // // Returns the image as an unsigned character array containing // imageWidth*imageHeight*3 entries (for that many bytes of storage). // // NOTE: this function expects imageData to be UNALLOCATED, and will allocate // memory itself. If the function fails (returns false), imageData // will be set to NULL and any allocated memory will be automatically deallocated. // //////////////////////////////////////////////////////////////////////////////// bool readPPM(string filename, int &imageWidth, int &imageHeight, unsigned char* &imageData) { FILE *fp = fopen(filename.c_str(), "r"); int temp, maxValue; fscanf(fp, "P%d", &temp); if(temp != 3) { fprintf(stderr, "Error: PPM file is not of correct format! (Must be P3, is P%d.)\n", temp); fclose(fp); return false; } //got the file header right... fscanf(fp, "%d", &imageWidth); fscanf(fp, "%d", &imageHeight); fscanf(fp, "%d", &maxValue); //now that we know how big it is, allocate the buffer... imageData = new unsigned char[imageWidth*imageHeight*3]; if(!imageData) { fprintf(stderr, "Error: couldn't allocate image memory. Dimensions: %d x %d.\n", imageWidth, imageHeight); fclose(fp); return false; } //and read the data in. for(int j = 0; j < imageHeight; j++) { for(int i = 0; i < imageWidth; i++) { int r, g, b; fscanf(fp, "%d", &r); fscanf(fp, "%d", &g); fscanf(fp, "%d", &b); imageData[(j*imageWidth+i)*3+0] = r; imageData[(j*imageWidth+i)*3+1] = g; imageData[(j*imageWidth+i)*3+2] = b; } } fclose(fp); return true; } // readAlphaPPM() ////////////////////////////////////////////////////////////// // // Very simple PPM reader, except only stores one channel of information, and // the image is presumed to correspond to an alpha map. Yes, this functionality // could (and probably should) be combined with the regular PPM function; // there's a lot of functional overlap. // //////////////////////////////////////////////////////////////////////////////// bool readAlphaPPM(string filename, int &imageWidth, int &imageHeight, unsigned char* &charMask) { FILE *fp = fopen(filename.c_str(), "r"); int temp, maxValue; fscanf(fp, "P%d", &temp); if(temp != 3) { fprintf(stderr, "Error: PPM file is not of correct format! (Must be P3, is P%d.)\n", temp); fclose(fp); return false; } //got the file header right... fscanf(fp, "%d", &imageWidth); fscanf(fp, "%d", &imageHeight); fscanf(fp, "%d", &maxValue); //now that we know how big it is, allocate the buffer... charMask = new unsigned char[imageWidth*imageHeight]; if(!charMask) { fprintf(stderr, "Error: couldn't allocate image memory. Dimensions: %d x %d.\n", imageWidth, imageHeight); fclose(fp); return false; } //and read the data in. for(int j = 0; j < imageHeight; j++) { for(int i = 0; i < imageWidth; i++) { int t; fscanf(fp, "%d", &t); charMask[j*imageWidth+i] = (unsigned char)t; fscanf(fp, "%d", &t); fscanf(fp, "%d", &t); } } fclose(fp); return true; } // registerOpenGLTexture() ///////////////////////////////////////////////////// // // Attempts to register an image buffer with OpenGL. Sets the texture handle // appropriately upon success, and uses a number of default texturing // values. This function only provides the basics: just sets up this image // as an unrepeated 2D texture. // //////////////////////////////////////////////////////////////////////////////// bool registerTransparentOpenGLTexture(unsigned char *imageData, unsigned char *imageMask, int texWidth, int texHeight, GLuint &texHandle) { unsigned char *fullData = new unsigned char[texWidth*texHeight*4]; for(int j = 0; j < texHeight; j++) { for(int i = 0; i < texWidth; i++) { fullData[(j*texWidth+i)*4+0] = imageData[(j*texWidth+i)*3+0]; fullData[(j*texWidth+i)*4+1] = imageData[(j*texWidth+i)*3+1]; fullData[(j*texWidth+i)*4+2] = imageData[(j*texWidth+i)*3+2]; fullData[(j*texWidth+i)*4+3] = imageMask[(j*texWidth+i)]; } } //first off, get a real texture handle. glGenTextures(1, &texHandle); //make sure that future texture functions affect this handle glBindTexture(GL_TEXTURE_2D, texHandle); //set this texture's color to be multiplied by the surface colors -- // GL_MODULATE instead of GL_REPLACE allows us to take advantage of OpenGL lighting glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); //set the minification ang magnification functions to be linear; not perfect // but much better than nearest-texel (GL_NEAREST). glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //Set the texture to repeat in S and T -- though it doesn't matter here // because our texture coordinates are always in [0,0] to [1,1]. glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); //actually transfer the texture to the GPU! glTexImage2D(GL_TEXTURE_2D, //still working with 2D textures, obv. 0, //not using mipmapping, so this is the highest level. GL_RGBA, //we're going to provide OpenGL with R, G, and B components... texWidth, //...of this width... texHeight, //...and this height. 0, //give it a border of 0 pixels (none) GL_RGBA, //and store it, internally, as RGB (OpenGL will convert to floats between 0.0 and 1.0f) GL_UNSIGNED_BYTE, //this is the format our data is in, and finally, fullData); //there's the data! delete fullData; //whoops! i guess this function can't fail. in an ideal world, there would //be checks with glGetError() that we could use to determine if this failed. return true; } // recomputeOrientation() ////////////////////////////////////////////////////// // // This function updates the camera's position in cartesian coordinates based // on its position in spherical coordinates. Should be called every time // cameraTheta, cameraPhi, or cameraRadius is updated. // //////////////////////////////////////////////////////////////////////////////// void recomputeOrientation() { dirX = sinf(cameraTheta)*sinf(cameraPhi); dirZ = -cosf(cameraTheta)*sinf(cameraPhi); dirY = cosf(cameraPhi); glutPostRedisplay(); } // resizeWindow() ////////////////////////////////////////////////////////////// // // GLUT callback for window resizing. Resets GL_PROJECTION matrix and viewport. // //////////////////////////////////////////////////////////////////////////////// void resizeWindow(int w, int h) { aspectRatio = w / (float)h; windowWidth = w; windowHeight = h; //update the viewport to fill the window glViewport(0, 0, w, h); //update the projection matrix with the new window properties glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0,aspectRatio,0.1,100000); } // mouseCallback() ///////////////////////////////////////////////////////////// // // GLUT callback for mouse clicks. We save the state of the mouse button // when this is called so that we can check the status of the mouse // buttons inside the motion callback (whether they are up or down). // //////////////////////////////////////////////////////////////////////////////// void mouseCallback(int button, int state, int thisX, int thisY) { //update the left and right mouse button states, if applicable if(button == GLUT_LEFT_BUTTON) leftMouseButton = state; else if(button == GLUT_RIGHT_BUTTON) rightMouseButton = state; //and update the last seen X and Y coordinates of the mouse mouseX = thisX; mouseY = thisY; } // mouseMotion() /////////////////////////////////////////////////////////////// // // GLUT callback for mouse movement. We update cameraPhi, cameraTheta, and/or // cameraRadius based on how much the user has moved the mouse in the // X or Y directions (in screen space) and whether they have held down // the left or right mouse buttons. If the user hasn't held down any // buttons, the function just updates the last seen mouse X and Y coords. // //////////////////////////////////////////////////////////////////////////////// void mouseMotion(int x, int y) { if(leftMouseButton == GLUT_DOWN) { cameraTheta += (x - mouseX)*0.005; cameraPhi += (y - mouseY)*0.005; // make sure that phi stays within the range (0, M_PI) if(cameraPhi <= 0) cameraPhi = 0+0.001; if(cameraPhi >= M_PI) cameraPhi = M_PI-0.001; recomputeOrientation(); //update camera (x,y,z) based on (radius,theta,phi) } mouseX = x; mouseY = y; } // initScene() ///////////////////////////////////////////////////////////////// // // A basic scene initialization function; should be called once after the // OpenGL context has been created. Doesn't need to be called further. // //////////////////////////////////////////////////////////////////////////////// void initScene() { glEnable(GL_DEPTH_TEST); float lightCol[4] = { 1, 1, 1, 1}; float ambientCol[4] = {0.3, 0.3, 0.3, 1.0}; glLightfv(GL_LIGHT0,GL_DIFFUSE,lightCol); glLightfv(GL_LIGHT0, GL_AMBIENT, ambientCol); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); float rangeX = 50.0f; float rangeZ = 50.0f; int numSprites = 100; for(int i = 0; i < numSprites; i++) { grassSpritePositions.push_back(pair( rand()/(float)RAND_MAX * rangeX - rangeX/2.0f, //rand()/(float)RAND_MAX * rangeZ - rangeZ/2.0f), (rangeZ * (i/(float)numSprites)) - rangeZ / 2.0f)); } //glShadeModel(GL_SMOOTH); glShadeModel(GL_FLAT); } // renderScene() /////////////////////////////////////////////////////////////// // // GLUT callback for scene rendering. Sets up the modelview matrix, renders // a teapot to the back buffer, and switches the back buffer with the // front buffer (what the user sees). // //////////////////////////////////////////////////////////////////////////////// void renderScene(void) { //update the modelview matrix based on the camera's position glMatrixMode(GL_MODELVIEW); //make sure we aren't changing the projection matrix! glLoadIdentity(); gluLookAt(cameraX, cameraY, cameraZ,//camera is located at (x,y,z) cameraX+dirX, cameraY+dirY, cameraZ+dirZ, 0.0f,1.0f,0.0f); //up vector is (0,1,0) (positive Y) //update the light position after we set the modelview matrix; //lights get transformed by the modelview matrix just like every other point! float lPosition[4] = { 1000, 1000, 1000, 1 }; float lAmbient[4] = { 1.0f,1.0f,1.0f,1.0f}; glLightfv(GL_LIGHT0,GL_POSITION,lPosition); glLightfv(GL_LIGHT0, GL_AMBIENT, lAmbient); //enable OpenGL blending! glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //clear the render buffer glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); //enable textures. glEnable(GL_TEXTURE_2D); glBindTexture(GL_TEXTURE_2D, grassTexHandle); glUseProgram(shaderProgramHandle); glUniform1f(timeLocation, glutGet(GLUT_ELAPSED_TIME)/1000.0f); //somewhat hacky note: the grass sprites are all sorted by their X position so we don't run into //any transparent rendering problems. In the real world, they would have to be sorted dynamically. float spriteXWidth = 5.0f; float spriteYWidth = 3.25; glColor4f(1.0f,1.0f,1.0f,1.0f); for(int i = 0; i < grassSpritePositions.size(); i++) { glBegin(GL_QUADS); glNormal3f(0,0,1); glTexCoord2f(0,0); glVertex3f(grassSpritePositions[i].first-(spriteXWidth/2.0f), spriteYWidth, grassSpritePositions[i].second); glNormal3f(0,0,1); glTexCoord2f(1,0); glVertex3f(grassSpritePositions[i].first+(spriteXWidth/2.0f), spriteYWidth, grassSpritePositions[i].second); glNormal3f(0,0,1); glTexCoord2f(1,1); glVertex3f(grassSpritePositions[i].first+(spriteXWidth/2.0f), 0.0f, grassSpritePositions[i].second); glNormal3f(0,0,1); glTexCoord2f(0,1); glVertex3f(grassSpritePositions[i].first-(spriteXWidth/2.0f), 0.0f, grassSpritePositions[i].second); glEnd(); } //push the back buffer to the screen glutSwapBuffers(); } // readTextFile() ////////////////////////////////////////////////////////////// // // Reads in a text file as a single string. Used to aid in shader loading. // //////////////////////////////////////////////////////////////////////////////// void readTextFile(string filename, char* &output) { string buf = string(""); string line; ifstream in(filename.c_str()); while(getline(in, line)) buf += line + "\n"; output = new char[buf.length()+1]; strncpy(output, buf.c_str(), buf.length()); output[buf.length()] = '\0'; in.close(); } // setupShaders() ////////////////////////////////////////////////////////////// // // A simple helper subrouting that performs the necessary steps to enable shaders // and bind them appropriately. Note because we're using global variables, // everything is relatively hard-coded, including filenames, and none of // the information are passed to the function as parameters. // //////////////////////////////////////////////////////////////////////////////// void setupShaders() { readTextFile("texture.vert", vertexShaderString); readTextFile("texture.frag", fragmentShaderString); vertexShaderHandle = glCreateShader(GL_VERTEX_SHADER); fragmentShaderHandle = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(vertexShaderHandle, 1, (const char**)&vertexShaderString, NULL); glShaderSource(fragmentShaderHandle, 1, (const char**)&fragmentShaderString, NULL); //free up that memory cause we're awesome programmers. delete [] vertexShaderString; delete [] fragmentShaderString; glCompileShader(vertexShaderHandle); glCompileShader(fragmentShaderHandle); shaderProgramHandle = glCreateProgram(); glAttachShader(shaderProgramHandle, vertexShaderHandle); glAttachShader(shaderProgramHandle, fragmentShaderHandle); glLinkProgram(shaderProgramHandle); glUseProgram(shaderProgramHandle); timeLocation = glGetUniformLocation(shaderProgramHandle, "time"); } // normalKeys() //////////////////////////////////////////////////////////////// // // GLUT keyboard callback. // //////////////////////////////////////////////////////////////////////////////// void normalKeys(unsigned char key, int x, int y) { if(key == 'q' || key == 'Q') exit(0); //basic controls for a free-view camera. if(key == 'w' || key == 'W') { cameraX += dirX; cameraY += dirY; cameraZ += dirZ; } if(key == 's' || key == 'S') { cameraX -= dirX; cameraY -= dirY; cameraZ -= dirZ; } } // main() ////////////////////////////////////////////////////////////////////// // // Program entry point. Does not process command line arguments. // //////////////////////////////////////////////////////////////////////////////// int main(int argc, char **argv) { //create a double-buffered GLUT window at (50,50) with predefined windowsize glutInit(&argc, argv); glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA); glutInitWindowPosition(50,50); glutInitWindowSize(windowWidth,windowHeight); glutCreateWindow("how soothing"); //give the camera a 'pretty' starting point! //cameraRadius = 7.0f; cameraX = cameraY = cameraZ = 10.0f; cameraTheta = -1.00; cameraPhi = 2.0; recomputeOrientation(); //register callback functions... glutKeyboardFunc(normalKeys); glutDisplayFunc(renderScene); glutIdleFunc(renderScene); glutReshapeFunc(resizeWindow); glutMouseFunc(mouseCallback); glutMotionFunc(mouseMotion); //do some basic OpenGL setup initScene(); glewInit(); setupShaders(); //read in our file and register our texture bool success = readPPM("blades.ppm", grassTexWidth, grassTexHeight, grassTexData); readAlphaPPM("blades_mask.ppm", grassTexWidth, grassTexHeight, grassMaskData); //NOTE THAT THIS NEEDS TO HAPPEN AFTER THE OPENGL CONTEXT HAS BEEN INITIALIZED. // which it has, thankfully. if(success) registerTransparentOpenGLTexture(grassTexData, grassMaskData, grassTexWidth, grassTexHeight, grassTexHandle); //and enter the GLUT loop, never to exit. glutMainLoop(); return(0); }