//----------------------------------------------------------------------------- // Product: OpenCTM tools // File: ctmviewer.cpp // Description: 3D file viewer. The program can be used to view OpenCTM files // in an interactive OpenGL window. Files in other supported // formats can also be viewed. //----------------------------------------------------------------------------- // Copyright (c) 2009-2010 Marcus Geelnard // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // // 2. Altered source versions must be plainly marked as such, and must not // be misrepresented as being the original software. // // 3. This notice may not be removed or altered from any source // distribution. //----------------------------------------------------------------------------- #include #include #include #include #include #include #ifdef __APPLE_CC__ #include #else #include #endif #include #include "mesh.h" #include "meshio.h" #include "sysdialog.h" #include "systimer.h" #include "image.h" #include "common.h" using namespace std; // We need PI #ifndef PI #define PI 3.141592653589793238462643f #endif // Configuration constants #define FOCUS_TIME 0.1 #define DOUBLE_CLICK_TIME 0.25 //----------------------------------------------------------------------------- // GLSL source code (generated from source by bin2c) //----------------------------------------------------------------------------- #include "phong_vert.h" #include "phong_frag.h" //----------------------------------------------------------------------------- // Icon bitmaps //----------------------------------------------------------------------------- #include "icons/icon_open.h" #include "icons/icon_save.h" #include "icons/icon_texture.h" #include "icons/icon_help.h" //----------------------------------------------------------------------------- // The GLViewer application class (declaration) //----------------------------------------------------------------------------- class GLButton; class GLViewer { private: // File information for the current mesh string mFileName, mFilePath; long mFileSize; // Window state cariables int mWidth, mHeight; GLint mDepthBufferResolution; int mOldMouseX, mOldMouseY; double mLastClickTime; bool mMouseRotate; bool mMouseZoom; bool mMousePan; bool mFocusing; Vector3 mFocusStartPos; Vector3 mFocusEndPos; double mFocusStartTime; double mFocusEndTime; double mFocusStartDistance; double mFocusEndDistance; // Camera state Vector3 mCameraPosition; Vector3 mCameraLookAt; Vector3 mCameraUp; GLdouble mModelviewMatrix[16]; GLdouble mProjectionMatrix[16]; GLint mViewport[4]; // Mesh information Mesh * mMesh; Vector3 mAABBMin, mAABBMax; GLuint mDisplayList; GLuint mTexHandle; // Polygon rendering mode (fill / line) GLenum mPolyMode; // GLSL objects bool mUseShader; GLuint mShaderProgram; GLuint mVertShader; GLuint mFragShader; // List of GUI buttons list mButtons; // Master timer resource SysTimer mTimer; /// Set up the camera. void SetupCamera(); /// Initialize the GLSL shader (requires OpenGL 2.0 or better). void InitShader(); /// Initialize the texture. void InitTexture(const char * aFileName); /// Set up the scene lighting. void SetupLighting(); /// Set up the material. void SetupMaterial(); /// Draw a mesh void DrawMesh(Mesh * aMesh); /// Load a file to the mesh void LoadFile(const char * aFileName, const char * aOverrideTexture); /// Load a texture file void LoadTexture(const char * aFileName); /// Draw an outline box. void DrawOutlineBox(int x1, int y1, int x2, int y2, float r, float g, float b, float a); /// Draw a string using GLUT. The string is shown on top of an alpha-blended /// quad. void DrawString(string aString, int x, int y); /// Draw 2D overlay void Draw2DOverlay(); /// Get 3D coordinate under the mouse cursor. bool WinCoordTo3DCoord(int x, int y, Vector3 &aPoint); /// Update the focus position of the camera. void UpdateFocus(); public: /// Constructor GLViewer(); /// Destructor ~GLViewer(); /// Open another file void ActionOpenFile(); /// Save the file void ActionSaveFile(); /// Open a texture file void ActionOpenTexture(); /// Toggle wire frame view on/off void ActionToggleWireframe(); /// Fit model to the screen (re-focus) void ActionFitToScreen(); /// Set camera up direction to Y void ActionCameraUpY(); /// Set camera up direction to Z void ActionCameraUpZ(); /// Zoom camera one step in void ActionZoomIn(); /// Zoom camera one step out void ActionZoomOut(); /// Exit program void ActionExit(); /// Show a help dialog void ActionHelp(); /// Redraw function. void WindowRedraw(void); /// Resize function. void WindowResize(int w, int h); /// Mouse click function void MouseClick(int button, int state, int x, int y); /// Mouse move function void MouseMove(int x, int y); /// Keyboard function void KeyDown(unsigned char key, int x, int y); /// Keyboard function (special keys) void SpecialKeyDown(int key, int x, int y); /// Run the application void Run(int argc, char **argv); }; //----------------------------------------------------------------------------- // A class for OpenGL rendered GUI buttons //----------------------------------------------------------------------------- class GLButton { private: // Texture handle GLuint mTexHandle; // Highlight on/off bool mHighlight; public: /// Constructor. GLButton() { mTexHandle = 0; mPosX = 0; mPosY = 0; mWidth = 32; mHeight = 32; mHighlight = false; mParent = NULL; } /// Destructor. virtual ~GLButton() { if(mTexHandle) glDeleteTextures(1, &mTexHandle); } /// Set glyph for this button. void SetGlyph(const unsigned char * aBitmap, int aWidth, int aHeight, int aComponents) { // Update the button size mWidth = aWidth; mHeight = aHeight; // Upload the texture to OpenGL if(mTexHandle) glDeleteTextures(1, &mTexHandle); glGenTextures(1, &mTexHandle); if(mTexHandle) { // Determine the color format GLuint format; if(aComponents == 3) format = GL_RGB; else if(aComponents == 4) format = GL_RGBA; else format = GL_LUMINANCE; glBindTexture(GL_TEXTURE_2D, mTexHandle); if(GLEW_VERSION_1_4) { // Generate mipmaps automatically and use them glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); } else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); } glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexImage2D(GL_TEXTURE_2D, 0, aComponents, aWidth, aHeight, 0, format, GL_UNSIGNED_BYTE, (GLvoid *) aBitmap); } } /// Redraw function. void Redraw() { // Set opacity of the icon if(mHighlight) glColor4f(1.0f, 1.0f, 1.0f, 1.0f); else glColor4f(1.0f, 1.0f, 1.0f, 0.7f); // Enable texturing if(mTexHandle) { glBindTexture(GL_TEXTURE_2D, mTexHandle); glEnable(GL_TEXTURE_2D); } // Enable blending glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Draw the icon as a textured quad glBegin(GL_QUADS); glTexCoord2f(0.0f, 0.0f); glVertex2i(mPosX, mPosY); glTexCoord2f(1.0f, 0.0f); glVertex2i(mPosX + mWidth, mPosY); glTexCoord2f(1.0f, 1.0f); glVertex2i(mPosX + mWidth, mPosY + mHeight); glTexCoord2f(0.0f, 1.0f); glVertex2i(mPosX, mPosY + mHeight); glEnd(); // We're done glDisable(GL_BLEND); glDisable(GL_TEXTURE_2D); } /// Mouse move function. The function returns true if the state of the /// button has changed. bool MouseMove(int x, int y) { bool hit = (x >= mPosX) && (x < (mPosX + mWidth)) && (y >= mPosY) && (y < (mPosY + mHeight)); bool changed = (mHighlight != hit); mHighlight = hit; return changed; } /// Mouse click function. bool MouseClick(int aState, int x, int y) { bool hit = (x >= mPosX) && (x < (mPosX + mWidth)) && (y >= mPosY) && (y < (mPosY + mHeight)); if(hit && (aState == GLUT_DOWN)) DoAction(); return hit; } /// The action function that will be performed when a button is clicked. virtual void DoAction() {} GLint mPosX, mPosY; GLint mWidth, mHeight; GLViewer * mParent; }; //----------------------------------------------------------------------------- // Customized button classes (implementing different actions) //----------------------------------------------------------------------------- class OpenButton: public GLButton { public: void DoAction() { if(!mParent) return; mParent->ActionOpenFile(); } }; class SaveButton: public GLButton { public: void DoAction() { if(!mParent) return; mParent->ActionSaveFile(); } }; class OpenTextureButton: public GLButton { public: void DoAction() { if(!mParent) return; mParent->ActionOpenTexture(); } }; class HelpButton: public GLButton { public: void DoAction() { if(!mParent) return; mParent->ActionHelp(); } }; //----------------------------------------------------------------------------- // GLUT callback function prototypes //----------------------------------------------------------------------------- void GLUTWindowRedraw(void); void GLUTWindowResize(int w, int h); void GLUTMouseClick(int button, int state, int x, int y); void GLUTMouseMove(int x, int y); void GLUTKeyDown(unsigned char key, int x, int y); void GLUTSpecialKeyDown(int key, int x, int y); //----------------------------------------------------------------------------- // GLViewer: OpenGL related functions //----------------------------------------------------------------------------- /// Set up the camera. void GLViewer::SetupCamera() { if(mMesh) mMesh->BoundingBox(mAABBMin, mAABBMax); else { mAABBMin = Vector3(-1.0f, -1.0f, -1.0f); mAABBMax = Vector3(1.0f, 1.0f, 1.0f); } mCameraLookAt = (mAABBMax + mAABBMin) * 0.5f; float delta = (mAABBMax - mAABBMin).Abs(); if(mCameraUp.z > 0.0f) mCameraPosition = Vector3(mCameraLookAt.x, mCameraLookAt.y - 0.8f * delta, mCameraLookAt.z + 0.2f * delta); else mCameraPosition = Vector3(mCameraLookAt.x, mCameraLookAt.y + 0.2f * delta, mCameraLookAt.z + 0.8f * delta); } /// Initialize the GLSL shader (requires OpenGL 2.0 or better). void GLViewer::InitShader() { const GLchar * src[1]; // Load vertex shader mVertShader = glCreateShader(GL_VERTEX_SHADER); src[0] = (const GLchar *) phongVertSrc; glShaderSource(mVertShader, 1, src, NULL); // Load fragment shader mFragShader = glCreateShader(GL_FRAGMENT_SHADER); src[0] = (const GLchar *) phongFragSrc; glShaderSource(mFragShader, 1, src, NULL); int status; // Compile the vertex shader glCompileShader(mVertShader); glGetShaderiv(mVertShader, GL_COMPILE_STATUS, &status); if(!status) throw runtime_error("Could not compile vertex shader."); // Compile the fragment shader glCompileShader(mFragShader); glGetShaderiv(mFragShader, GL_COMPILE_STATUS, &status); if(!status) throw runtime_error("Could not compile fragment shader."); // Link the shader program mShaderProgram = glCreateProgram(); glAttachShader(mShaderProgram, mVertShader); glAttachShader(mShaderProgram, mFragShader); glLinkProgram(mShaderProgram); glGetProgramiv(mShaderProgram, GL_LINK_STATUS, &status); if(!status) throw runtime_error("Could not link shader program."); mUseShader = true; } /// Initialize the texture. void GLViewer::InitTexture(const char * aFileName) { Image image; // Load texture from a file if(aFileName) { // Check if file exists, and determine actual file name (relative or absolute) bool fileExists = false; string name = string(aFileName); FILE * inFile = fopen(name.c_str(), "rb"); if(inFile) fileExists = true; else if(mFilePath.size() > 0) { // Try the same path as the mesh file name = mFilePath + string(aFileName); inFile = fopen(name.c_str(), "rb"); if(inFile) fileExists = true; } if(inFile) fclose(inFile); if(fileExists) { cout << "Loading texture (" << aFileName << ")..." << endl; try { image.LoadFromFile(name.c_str()); } catch(exception &e) { cout << "Error loading texture: " << e.what() << endl; image.Clear(); } } } // If no texture was loaded if(image.IsEmpty()) { cout << "Loading texture (dummy)..." << endl; // Create a default, synthetic texture image.SetSize(256, 256, 1); for(int y = 0; y < image.mHeight; ++ y) { for(int x = 0; x < image.mWidth; ++ x) { if(((x & 0x000f) == 0) || ((y & 0x000f) == 0)) image.mData[y * image.mWidth + x] = 192; else image.mData[y * image.mWidth + x] = 255; } } } // Upload the texture to OpenGL if(!image.IsEmpty()) glGenTextures(1, &mTexHandle); else mTexHandle = 0; if(mTexHandle) { // Determine the color format GLuint format; if(image.mComponents == 3) format = GL_RGB; else if(image.mComponents == 4) format = GL_RGBA; else format = GL_LUMINANCE; glBindTexture(GL_TEXTURE_2D, mTexHandle); if(GLEW_VERSION_1_4) { // Generate mipmaps automatically and use them glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); } else { glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); } glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glTexImage2D(GL_TEXTURE_2D, 0, image.mComponents, image.mWidth, image.mHeight, 0, format, GL_UNSIGNED_BYTE, (GLvoid *) &image.mData[0]); } } /// Set up the scene lighting. void GLViewer::SetupLighting() { GLfloat pos[4], ambient[4], diffuse[4], specular[4]; // Set scene lighting properties glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE); glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE); ambient[0] = 0.2; ambient[1] = 0.2; ambient[2] = 0.2; ambient[3] = 1.0; glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient); // Set-up head light (GL_LIGHT0) pos[0] = mCameraPosition.x; pos[1] = mCameraPosition.y; pos[2] = mCameraPosition.z; pos[3] = 1.0f; glLightfv(GL_LIGHT0, GL_POSITION, pos); ambient[0] = 0.0f; ambient[1] = 0.0f; ambient[2] = 0.0f; ambient[3] = 1.0f; glLightfv(GL_LIGHT0, GL_AMBIENT, ambient); diffuse[0] = 0.8f; diffuse[1] = 0.8f; diffuse[2] = 0.8f; diffuse[3] = 1.0f; glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuse); specular[0] = 1.0f; specular[1] = 1.0f; specular[2] = 1.0f; specular[3] = 1.0f; glLightfv(GL_LIGHT0, GL_SPECULAR, specular); glEnable(GL_LIGHT0); } /// Set up the material. void GLViewer::SetupMaterial() { GLfloat specular[4], emission[4]; // Set up the material specular[0] = 0.3f; specular[1] = 0.3f; specular[2] = 0.3f; specular[3] = 1.0f; glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular); emission[0] = 0.0f; emission[1] = 0.0f; emission[2] = 0.0f; emission[3] = 1.0f; glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, emission); glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 40.0f); // Use color material for the diffuse and ambient components glColorMaterial(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE); glEnable(GL_COLOR_MATERIAL); } /// Draw a mesh void GLViewer::DrawMesh(Mesh * aMesh) { // We always have vertices glVertexPointer(3, GL_FLOAT, 0, &aMesh->mVertices[0]); glEnableClientState(GL_VERTEX_ARRAY); // Do we have normals? if(aMesh->mNormals.size() == aMesh->mVertices.size()) { glNormalPointer(GL_FLOAT, 0, &aMesh->mNormals[0]); glEnableClientState(GL_NORMAL_ARRAY); } // Do we have texture coordinates? if(aMesh->mTexCoords.size() == aMesh->mVertices.size()) { glTexCoordPointer(2, GL_FLOAT, 0, &aMesh->mTexCoords[0]); glEnableClientState(GL_TEXTURE_COORD_ARRAY); } // Do we have colors? if(aMesh->mColors.size() == aMesh->mVertices.size()) { glColorPointer(4, GL_FLOAT, 0, &aMesh->mColors[0]); glEnableClientState(GL_COLOR_ARRAY); } // Use glDrawElements to draw the triangles... glShadeModel(GL_SMOOTH); if(GLEW_VERSION_1_2) glDrawRangeElements(GL_TRIANGLES, 0, aMesh->mVertices.size() - 1, aMesh->mIndices.size(), GL_UNSIGNED_INT, &aMesh->mIndices[0]); else glDrawElements(GL_TRIANGLES, aMesh->mIndices.size(), GL_UNSIGNED_INT, &aMesh->mIndices[0]); // We do not use the client state anymore... glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_NORMAL_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glDisableClientState(GL_COLOR_ARRAY); } // Load a file to the mesh void GLViewer::LoadFile(const char * aFileName, const char * aOverrideTexture) { // Get the file size ifstream f(aFileName, ios::in | ios::binary); if(f.fail()) throw runtime_error("Unable to open the file."); f.seekg(0, ios_base::end); long tmpFileSize = (long) f.tellg(); f.close(); // Load the mesh cout << "Loading " << aFileName << "..." << flush; mTimer.Push(); Mesh * newMesh = new Mesh(); try { ImportMesh(aFileName, newMesh); } catch(exception &e) { delete newMesh; throw; } if(mMesh) delete mMesh; mMesh = newMesh; cout << "done (" << int(mTimer.PopDelta() * 1000.0 + 0.5) << " ms)" << endl; // Get the file name (excluding the path), and the path (excluding the file name) mFileName = ExtractFileName(string(aFileName)); mFilePath = ExtractFilePath(string(aFileName)); // The temporary file size is now the official file size... mFileSize = tmpFileSize; // Set window title string windowCaption = string("OpenCTM viewer - ") + mFileName; glutSetWindowTitle(windowCaption.c_str()); // If the file did not contain any normals, calculate them now... if(mMesh->mNormals.size() != mMesh->mVertices.size()) { cout << "Calculating normals..." << flush; mTimer.Push(); mMesh->CalculateNormals(); cout << "done (" << int(mTimer.PopDelta() * 1000.0 + 0.5) << " ms)" << endl; } // Load the texture if(mTexHandle) glDeleteTextures(1, &mTexHandle); mTexHandle = 0; if(mMesh->mTexCoords.size() == mMesh->mVertices.size()) { string texFileName = mMesh->mTexFileName; if(aOverrideTexture) texFileName = string(aOverrideTexture); if(texFileName.size() > 0) InitTexture(texFileName.c_str()); else InitTexture(0); } // Setup texture parameters for the shader if(mUseShader) { glUseProgram(mShaderProgram); // Set the uUseTexture uniform GLint useTexLoc = glGetUniformLocation(mShaderProgram, "uUseTexture"); if(useTexLoc >= 0) glUniform1i(useTexLoc, glIsTexture(mTexHandle)); // Set the uTex uniform GLint texLoc = glGetUniformLocation(mShaderProgram, "uTex"); if(texLoc >= 0) glUniform1i(texLoc, 0); glUseProgram(0); } // Load the mesh into a displaylist if(mDisplayList) glDeleteLists(mDisplayList, 1); mDisplayList = glGenLists(1); glNewList(mDisplayList, GL_COMPILE); DrawMesh(mMesh); glEndList(); // Init the camera for the new mesh mCameraUp = Vector3(0.0f, 0.0f, 1.0f); SetupCamera(); } // Load a texture file void GLViewer::LoadTexture(const char * aFileName) { // Load the texture if(mTexHandle) glDeleteTextures(1, &mTexHandle); mTexHandle = 0; if(mMesh->mTexCoords.size() == mMesh->mVertices.size()) InitTexture(aFileName); // Setup texture parameters for the shader if(mUseShader) { glUseProgram(mShaderProgram); // Set the uUseTexture uniform GLint useTexLoc = glGetUniformLocation(mShaderProgram, "uUseTexture"); if(useTexLoc >= 0) glUniform1i(useTexLoc, glIsTexture(mTexHandle)); // Set the uTex uniform GLint texLoc = glGetUniformLocation(mShaderProgram, "uTex"); if(texLoc >= 0) glUniform1i(texLoc, 0); glUseProgram(0); } } // Draw an outline box. void GLViewer::DrawOutlineBox(int x1, int y1, int x2, int y2, float r, float g, float b, float a) { // Draw a blended box // Note: We add (1,1) to the (x2,y2) corner to cover the entire pixel range glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glBegin(GL_QUADS); glColor4f(r, g, b, 0.7f * a); glVertex2i(x1, y1); glVertex2i(x2+1, y1); glColor4f(r, g, b, 0.7f * a + 0.3f); glVertex2i(x2+1, y2+1); glVertex2i(x1, y2+1); glEnd(); glDisable(GL_BLEND); // Draw a solid outline glPushMatrix(); glTranslatef(0.5f, 0.5f, 0.0f); // Compensate for 0.5 pixel center offset glColor4f(r, g, b, 1.0f); glBegin(GL_LINE_LOOP); glVertex2i(x1, y1-1); glVertex2i(x2, y1-1); glVertex2i(x2+1, y1); glVertex2i(x2+1, y2); glVertex2i(x2, y2+1); glVertex2i(x1, y2+1); glVertex2i(x1-1, y2); glVertex2i(x1-1, y1); glEnd(); glPopMatrix(); } // Draw a string using GLUT. The string is shown on top of an alpha-blended // quad. void GLViewer::DrawString(string aString, int x, int y) { // Calculate the size of the string box int x0 = x, y0 = y; int x1 = x0, y1 = y0; int x2 = x0, y2 = y0; for(unsigned int i = 0; i < aString.size(); ++ i) { int c = (int) aString[i]; if(c == (int) 10) { x2 = x; y2 += 13; } else if(c != (int) 13) { x2 += glutBitmapWidth(GLUT_BITMAP_8_BY_13, c); if(x2 > x1) x1 = x2; } } y1 = y2 + 13; // Draw a alpha blended box DrawOutlineBox(x0-4, y0-3, x1+4, y1+4, 0.3f, 0.3f, 0.3f, 0.6f); // Print the text glColor3f(1.0f, 1.0f, 1.0f); x2 = x; y2 = y + 13; for(unsigned int i = 0; i < aString.size(); ++ i) { int c = (int) aString[i]; if(c == (int) 10) { x2 = x; y2 += 13; } else if(c != (int) 13) { glRasterPos2i(x2, y2); glutBitmapCharacter(GLUT_BITMAP_8_BY_13, c); x2 += glutBitmapWidth(GLUT_BITMAP_8_BY_13, c); } } } // Draw 2D overlay void GLViewer::Draw2DOverlay() { // Setup the matrices for a width x height 2D screen glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(0.0, (double) mWidth, (double) mHeight, 0.0, -1.0, 1.0); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); // Setup the rendering pipeline for 2D rendering glDisable(GL_LIGHTING); glDisable(GL_DEPTH_TEST); // Render an info string if(mMesh) { stringstream s; s << mFileName << " (" << (mFileSize + 512) / 1024 << "KB)" << endl; s << mMesh->mVertices.size() << " vertices" << endl; s << mMesh->mIndices.size() / 3 << " triangles"; DrawString(s.str(), 10, mHeight - 50); } // Calculate buttons bounding box, and draw it as an outline box int x1 = 9999, y1 = 9999, x2 = 0, y2 = 0; for(list::iterator b = mButtons.begin(); b != mButtons.end(); ++ b) { if((*b)->mPosX < x1) x1 = (*b)->mPosX; if(((*b)->mPosX + (*b)->mWidth) > x2) x2 = (*b)->mPosX + (*b)->mWidth; if((*b)->mPosY < y1) y1 = (*b)->mPosY; if(((*b)->mPosY + (*b)->mHeight) > y2) y2 = (*b)->mPosY + (*b)->mHeight; } DrawOutlineBox(x1-5, y1-5, x2+5, y2+5, 0.3f, 0.3f, 0.3f, 0.6f); // Render all the buttons (last = on top) for(list::iterator b = mButtons.begin(); b != mButtons.end(); ++ b) (*b)->Redraw(); } /// Get 3D coordinate under the mouse cursor. bool GLViewer::WinCoordTo3DCoord(int x, int y, Vector3 &aPoint) { // Read back the depth value at at (x, y) GLfloat z = 0.0f; glReadPixels(x, mHeight - y, 1, 1, GL_DEPTH_COMPONENT, GL_FLOAT, (GLvoid *) &z); if((z > 0.0f) && (z < 1.0f)) { // Convert the window coordinate to space coordinates GLdouble objX, objY, objZ; gluUnProject((GLdouble) x, (GLdouble) (mHeight - y), (GLdouble) z, mModelviewMatrix, mProjectionMatrix, mViewport, &objX, &objY, &objZ); aPoint = Vector3((float) objX, (float) objY, (float) objZ); return true; } else return false; } /// Update the focus position of the camera. void GLViewer::UpdateFocus() { double w = (mTimer.GetTime() - mFocusStartTime) / (mFocusEndTime - mFocusStartTime); Vector3 dir = Normalize(mCameraPosition - mCameraLookAt); if(w < 1.0) { w = pow(w, 0.2); mCameraLookAt = mFocusStartPos + (mFocusEndPos - mFocusStartPos) * w; mCameraPosition = mCameraLookAt + dir * (mFocusStartDistance + (mFocusEndDistance - mFocusStartDistance) * w); } else { mCameraLookAt = mFocusEndPos; mCameraPosition = mCameraLookAt + dir * mFocusEndDistance; mFocusing = false; } glutPostRedisplay(); } //----------------------------------------------------------------------------- // Actions (user activated functions) //----------------------------------------------------------------------------- /// Open another file void GLViewer::ActionOpenFile() { SysOpenDialog od; od.mFilters.push_back(string("All supported 3D files|*.ctm;*.ply;*.stl;*.3ds;*.dae;*.obj;*.lwo;*.off")); od.mFilters.push_back(string("OpenCTM (.ctm)|*.ctm")); od.mFilters.push_back(string("Stanford triangle format (.ply)|*.ply")); od.mFilters.push_back(string("Stereolitography (.stl)|*.stl")); od.mFilters.push_back(string("3D Studio (.3ds)|*.3ds")); od.mFilters.push_back(string("COLLADA (.dae)|*.dae")); od.mFilters.push_back(string("Wavefront geometry file (.obj)|*.obj")); od.mFilters.push_back(string("LightWave object (.lwo)|*.lwo")); od.mFilters.push_back(string("Geomview object file format (.off)|*.off")); if(od.Show()) { try { LoadFile(od.mFileName.c_str(), NULL); glutPostRedisplay(); } catch(exception &e) { SysMessageBox mb; mb.mMessageType = SysMessageBox::mtError; mb.mCaption = "Error"; mb.mText = string(e.what()); mb.Show(); } } } /// Save the file void GLViewer::ActionSaveFile() { if(!mMesh) { SysMessageBox mb; mb.mMessageType = SysMessageBox::mtError; mb.mCaption = "Save File"; mb.mText = string("No mesh has been loaded."); mb.Show(); return; } SysSaveDialog sd; sd.mFilters.push_back(string("All files|*")); sd.mFilters.push_back(string("OpenCTM (.ctm)|*.ctm")); sd.mFilters.push_back(string("Stanford triangle format (.ply)|*.ply")); sd.mFilters.push_back(string("Stereolitography (.stl)|*.stl")); sd.mFilters.push_back(string("3D Studio (.3ds)|*.3ds")); sd.mFilters.push_back(string("COLLADA (.dae)|*.dae")); sd.mFilters.push_back(string("Wavefront geometry file (.obj)|*.obj")); sd.mFilters.push_back(string("LightWave object (.lwo)|*.lwo")); sd.mFilters.push_back(string("Geomview object file format (.off)|*.off")); sd.mFilters.push_back(string("VRML 2.0 (.wrl)|*.wrl")); sd.mFileName = mFileName; if(sd.Show()) { try { Options opt; // Do not export normals that do not come from the original file if(!mMesh->mOriginalNormals) opt.mNoNormals = true; // Export the mesh ExportMesh(sd.mFileName.c_str(), mMesh, opt); } catch(exception &e) { SysMessageBox mb; mb.mMessageType = SysMessageBox::mtError; mb.mCaption = "Error"; mb.mText = string(e.what()); mb.Show(); } } } /// Open a texture file void GLViewer::ActionOpenTexture() { if(!mMesh || (mMesh->mTexCoords.size() < 1)) { SysMessageBox mb; mb.mMessageType = SysMessageBox::mtError; mb.mCaption = "Open Texture File"; mb.mText = string("This mesh does not have any texture coordinates."); mb.Show(); return; } SysOpenDialog od; od.mCaption = string("Open Texture File"); od.mFilters.push_back(string("All supported texture files|*.jpg;*.jpeg;*.png")); od.mFilters.push_back(string("JPEG|*.jpg;*.jpeg")); od.mFilters.push_back(string("PNG|*.png")); if(od.Show()) { try { LoadTexture(od.mFileName.c_str()); mMesh->mTexFileName = ExtractFileName(od.mFileName); glutPostRedisplay(); } catch(exception &e) { SysMessageBox mb; mb.mMessageType = SysMessageBox::mtError; mb.mCaption = "Error"; mb.mText = string(e.what()); mb.Show(); } } } /// Toggle wire frame view on/off void GLViewer::ActionToggleWireframe() { if(mPolyMode == GL_LINE) mPolyMode = GL_FILL; else mPolyMode = GL_LINE; glutPostRedisplay(); } /// Fit model to the screen (re-focus) void GLViewer::ActionFitToScreen() { double now = mTimer.GetTime(); mFocusStartTime = now; mFocusEndTime = now + FOCUS_TIME; mFocusStartPos = mCameraLookAt; mFocusStartDistance = (mCameraLookAt - mCameraPosition).Abs(); mFocusEndPos = (mAABBMax + mAABBMin) * 0.5f; mFocusEndDistance = 0.825 * (mAABBMax - mAABBMin).Abs(); mFocusing = true; UpdateFocus(); glutPostRedisplay(); } /// Set camera up direction to Y void GLViewer::ActionCameraUpY() { mCameraUp = Vector3(0.0f, 1.0f, 0.0f); SetupCamera(); glutPostRedisplay(); } /// Set camera up direction to Z void GLViewer::ActionCameraUpZ() { mCameraUp = Vector3(0.0f, 0.0f, 1.0f); SetupCamera(); glutPostRedisplay(); } /// Zoom camera one step in void GLViewer::ActionZoomIn() { double now = mTimer.GetTime(); mFocusStartTime = now; mFocusEndTime = now + FOCUS_TIME; mFocusStartPos = mCameraLookAt; mFocusStartDistance = (mCameraLookAt - mCameraPosition).Abs(); mFocusEndPos = mCameraLookAt; mFocusEndDistance = (1.0/1.5) * mFocusStartDistance; mFocusing = true; UpdateFocus(); glutPostRedisplay(); } /// Zoom camera one step out void GLViewer::ActionZoomOut() { double now = mTimer.GetTime(); mFocusStartTime = now; mFocusEndTime = now + FOCUS_TIME; mFocusStartPos = mCameraLookAt; mFocusStartDistance = (mCameraLookAt - mCameraPosition).Abs(); mFocusEndPos = mCameraLookAt; mFocusEndDistance = 1.5 * mFocusStartDistance; mFocusing = true; UpdateFocus(); glutPostRedisplay(); } /// Exit program void GLViewer::ActionExit() { // Note: In freeglut you can do glutLeaveMainLoop(), which is more graceful exit(0); } /// Show a help dialog void GLViewer::ActionHelp() { stringstream helpText; helpText << "ctmviewer - OpenCTM file viewer" << endl; helpText << "Copyright (c) 2009-2010 Marcus Geelnard" << endl << endl; helpText << "Keyboard actions:" << endl; helpText << " W - Toggle wire frame view on/off" << endl; helpText << " F - Fit model to the screen" << endl; helpText << " Y - Set Y as the up axis (change camera view)" << endl; helpText << " Z - Set Z as the up axis (change camera view)" << endl; helpText << " +/- - Zoom in/out with the camera" << endl; helpText << " ESC - Exit program" << endl << endl; helpText << "Mouse control:" << endl; helpText << " Left button - Rotate camera" << endl; helpText << " Middle button or wheel - Zoom camera" << endl; helpText << " Right button - Pan camera" << endl; helpText << " Double click - Focus on indicated surface"; SysMessageBox mb; mb.mMessageType = SysMessageBox::mtInformation; mb.mCaption = "Help"; mb.mText = helpText.str(); mb.Show(); } //----------------------------------------------------------------------------- // GLUT callback functions //----------------------------------------------------------------------------- /// Redraw function. void GLViewer::WindowRedraw(void) { // Get buffer properties glGetIntegerv(GL_DEPTH_BITS, &mDepthBufferResolution); // Set the viewport to be the entire window glViewport(0, 0, mWidth, mHeight); // Clear the buffer(s) glClear(GL_DEPTH_BUFFER_BIT); // Draw a gradient background glMatrixMode(GL_PROJECTION); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); glDisable(GL_LIGHTING); glDisable(GL_DEPTH_TEST); glBegin(GL_QUADS); glColor3f(0.4f, 0.5f, 0.7f); glVertex3f(-1.0f, -1.0f, 0.5f); glColor3f(0.3f, 0.4f, 0.7f); glVertex3f(1.0f, -1.0f, 0.5f); glColor3f(0.1f, 0.1f, 0.2f); glVertex3f(1.0f, 1.0f, 0.5f); glColor3f(0.1f, 0.15f, 0.24f); glVertex3f(-1.0f, 1.0f, 0.5f); glEnd(); // Calculate screen ratio (width / height) float ratio; if(mHeight == 0) ratio = 1.0f; else ratio = (float) mWidth / (float) mHeight; // Calculate optimal near and far Z clipping planes float farZ = (mAABBMax - mAABBMin).Abs() + (mCameraPosition - mCameraLookAt).Abs(); if(farZ < 1e-20f) farZ = 1e-20f; float nearZ; if(mDepthBufferResolution >= 24) nearZ = 0.0001f * farZ; else nearZ = 0.01f * farZ; // Set up perspective projection glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(60.0f, ratio, nearZ, farZ); // Set up the camera modelview matrix glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(mCameraPosition.x, mCameraPosition.y, mCameraPosition.z, mCameraLookAt.x, mCameraLookAt.y, mCameraLookAt.z, mCameraUp.x, mCameraUp.y, mCameraUp.z); // Read back camera matrices glGetDoublev(GL_MODELVIEW_MATRIX, mModelviewMatrix); glGetDoublev(GL_PROJECTION_MATRIX, mProjectionMatrix); glGetIntegerv(GL_VIEWPORT, mViewport); // Set up the lights SetupLighting(); // Enable material shader if(mUseShader) glUseProgram(mShaderProgram); else glEnable(GL_LIGHTING); // Draw the mesh SetupMaterial(); glEnable(GL_DEPTH_TEST); glPolygonMode(GL_FRONT_AND_BACK, mPolyMode); if(mTexHandle) { glBindTexture(GL_TEXTURE_2D, mTexHandle); glEnable(GL_TEXTURE_2D); glColor3f(1.0f, 1.0f, 1.0f); } else glColor3f(0.9f, 0.86f, 0.7f); if(mDisplayList) glCallList(mDisplayList); glDisable(GL_TEXTURE_2D); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // Disable material shader if(mUseShader) glUseProgram(0); else glDisable(GL_LIGHTING); // Draw 2D overlay (information text etc) Draw2DOverlay(); // Swap buffers glutSwapBuffers(); // Focusing? if(mFocusing) { UpdateFocus(); glutPostRedisplay(); } } /// Resize function. void GLViewer::WindowResize(int w, int h) { // Store the new window size mWidth = w; mHeight = h; } /// Mouse click function void GLViewer::MouseClick(int button, int state, int x, int y) { bool clickConsumed = false; if(button == GLUT_LEFT_BUTTON) { // Check if any of the GUI buttons were clicked for(list::iterator b = mButtons.begin(); b != mButtons.end(); ++ b) { if((*b)->MouseClick(state, x, y)) clickConsumed = true; } if(!clickConsumed) { if(state == GLUT_DOWN) { double now = mTimer.GetTime(); if((now - mLastClickTime) < DOUBLE_CLICK_TIME) { // Double click occured Vector3 mouseCoord3D; if(WinCoordTo3DCoord(x, y, mouseCoord3D)) { mFocusStartTime = now; mFocusEndTime = now + FOCUS_TIME; mFocusStartPos = mCameraLookAt; mFocusEndPos = mouseCoord3D; mFocusStartDistance = (mCameraLookAt - mCameraPosition).Abs(); mFocusEndDistance = mFocusStartDistance; mFocusing = true; } mLastClickTime = -1000.0; } else { // Single click occured mMouseRotate = true; mLastClickTime = now; } } else if(state == GLUT_UP) mMouseRotate = false; } } else if(button == GLUT_MIDDLE_BUTTON) { if(state == GLUT_DOWN) mMouseZoom = true; else if(state == GLUT_UP) mMouseZoom = false; } else if(button == GLUT_RIGHT_BUTTON) { if(state == GLUT_DOWN) mMousePan = true; else if(state == GLUT_UP) mMousePan = false; } else if(button == 3) // Mouse wheel up on some systems { if(state == GLUT_DOWN) ActionZoomIn(); } else if(button == 4) // Mouse wheel down on some systems { if(state == GLUT_DOWN) ActionZoomOut(); } mOldMouseX = x; mOldMouseY = y; // Focusing? if(mFocusing) { UpdateFocus(); glutPostRedisplay(); } } /// Mouse move function void GLViewer::MouseMove(int x, int y) { bool needsRedraw = false; float deltaX = (float) x - (float) mOldMouseX; float deltaY = (float) y - (float) mOldMouseY; mOldMouseX = x; mOldMouseY = y; if(mMouseRotate) { // Calculate delta angles float scale = 3.0f; if(mHeight > 0) scale /= (float) mHeight; float deltaTheta = -scale * deltaX; float deltaPhi = -scale * deltaY; // Adjust camera angles Vector3 viewVector = mCameraPosition - mCameraLookAt; float r = sqrtf(viewVector.x * viewVector.x + viewVector.y * viewVector.y + viewVector.z * viewVector.z); float phi, theta; if(r > 1e-20f) { if(mCameraUp.z > 0.0f) { phi = acosf(viewVector.z / r); theta = atan2f(viewVector.y, viewVector.x); } else { phi = acosf(viewVector.y / r); theta = atan2f(-viewVector.z, viewVector.x); } } else { if(mCameraUp.z > 0.0f) phi = viewVector.z > 0.0f ? 0.05f * PI : 0.95f * PI; else phi = viewVector.y > 0.0f ? 0.05f * PI : 0.95f * PI; theta = 0.0f; } phi += deltaPhi; theta += deltaTheta; if(phi > (0.95f * PI)) phi = 0.95f * PI; else if(phi < (0.05f * PI)) phi = 0.05f * PI; // Update the camera position if(mCameraUp.z > 0.0f) { viewVector.x = r * cos(theta) * sin(phi); viewVector.y = r * sin(theta) * sin(phi); viewVector.z = r * cos(phi); } else { viewVector.x = r * cos(theta) * sin(phi); viewVector.y = r * cos(phi); viewVector.z = -r * sin(theta) * sin(phi); } mCameraPosition = mCameraLookAt + viewVector; needsRedraw = true; } else if(mMouseZoom) { // Calculate delta angles float scale = 3.0f; if(mHeight > 0) scale /= (float) mHeight; float zoom = scale * deltaY; // Adjust camera zoom Vector3 viewVector = mCameraPosition - mCameraLookAt; viewVector = viewVector * powf(2.0f, zoom); // Update the camera position mCameraPosition = mCameraLookAt + viewVector; needsRedraw = true; } else if(mMousePan) { // Calculate delta movement float scale = 1.0f * (mCameraPosition - mCameraLookAt).Abs(); if(mHeight > 0) scale /= (float) mHeight; float panX = scale * deltaX; float panY = scale * deltaY; // Calculate camera movement Vector3 viewDir = Normalize(mCameraPosition - mCameraLookAt); Vector3 rightDir = Normalize(Cross(viewDir, mCameraUp)); Vector3 upDir = Normalize(Cross(rightDir, viewDir)); Vector3 moveDelta = rightDir * panX + upDir * panY; // Update the camera position mCameraPosition += moveDelta; mCameraLookAt += moveDelta; needsRedraw = true; } else { // Call mouse move for all the GUI buttons for(list::iterator b = mButtons.begin(); b != mButtons.end(); ++ b) { if((*b)->MouseMove(x, y)) needsRedraw = true; } } // Redraw? if(needsRedraw) glutPostRedisplay(); } /// Keyboard function void GLViewer::KeyDown(unsigned char key, int x, int y) { if(key == 15) // CTRL+O ActionOpenFile(); else if(key == 19) // CTRL+S ActionSaveFile(); else if(key == 'w') ActionToggleWireframe(); else if(key == 'f') ActionFitToScreen(); else if(key == 'y') ActionCameraUpY(); else if(key == 'z') ActionCameraUpZ(); else if(key == '+') ActionZoomIn(); else if(key == '-') ActionZoomOut(); else if(key == 27) // ESC ActionExit(); } /// Keyboard function (special keys) void GLViewer::SpecialKeyDown(int key, int x, int y) { if(key == GLUT_KEY_F1) ActionHelp(); } //----------------------------------------------------------------------------- // Application main code //----------------------------------------------------------------------------- /// Constructor GLViewer::GLViewer() { // Clear internal state mFileName = ""; mFilePath = ""; mFileSize = 0; mWidth = 1; mHeight = 1; mDepthBufferResolution = 16; mOldMouseX = 0; mOldMouseY = 0; mMouseRotate = false; mMouseZoom = false; mMousePan = false; mCameraUp = Vector3(0.0f, 0.0f, 1.0f); mFocusStartPos = Vector3(0.0f, 0.0f, 0.0f); mFocusEndPos = Vector3(0.0f, 0.0f, 0.0f); mFocusStartTime = 0.0; mFocusEndTime = 0.0; mFocusStartDistance = 1.0; mFocusEndDistance = 1.0; mFocusing = false; mLastClickTime = -1000.0; mDisplayList = 0; mPolyMode = GL_FILL; mTexHandle = 0; mUseShader = false; mShaderProgram = 0; mVertShader = 0; mFragShader = 0; mMesh = NULL; } /// Destructor GLViewer::~GLViewer() { // Free all GUI buttons for(list::iterator b = mButtons.begin(); b != mButtons.end(); ++ b) delete (*b); // Free the mesh if(mMesh) delete mMesh; } /// Run the application void GLViewer::Run(int argc, char **argv) { try { // Init GLUT glutInit(&argc, argv); // Create the glut window glutInitWindowSize(640, 480); glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH); glutCreateWindow("OpenCTM viewer"); // Init GLEW (for OpenGL 2.x support) if(glewInit() != GLEW_OK) throw runtime_error("Unable to initialize GLEW."); // Load the phong shader, if we can if(GLEW_VERSION_2_0) InitShader(); else if(GLEW_VERSION_1_2) glLightModeli(GL_LIGHT_MODEL_COLOR_CONTROL, GL_SEPARATE_SPECULAR_COLOR); // Set the GLUT callback functions (these are bridged to the corresponding // class methods) glutReshapeFunc(GLUTWindowResize); glutDisplayFunc(GLUTWindowRedraw); glutMouseFunc(GLUTMouseClick); glutMotionFunc(GLUTMouseMove); glutPassiveMotionFunc(GLUTMouseMove); glutKeyboardFunc(GLUTKeyDown); glutSpecialFunc(GLUTSpecialKeyDown); // Create GUI buttons GLButton * b1 = new OpenButton(); mButtons.push_back(b1); b1->mParent = this; b1->SetGlyph(icon_open, 32, 32, 4); b1->mPosX = 12; b1->mPosY = 10; GLButton * b2 = new SaveButton(); mButtons.push_back(b2); b2->mParent = this; b2->SetGlyph(icon_save, 32, 32, 4); b2->mPosX = 60; b2->mPosY = 10; GLButton * b3 = new OpenTextureButton(); mButtons.push_back(b3); b3->mParent = this; b3->SetGlyph(icon_texture, 32, 32, 4); b3->mPosX = 108; b3->mPosY = 10; GLButton * b4 = new HelpButton(); mButtons.push_back(b4); b4->mParent = this; b4->SetGlyph(icon_help, 32, 32, 4); b4->mPosX = 156; b4->mPosY = 10; // Load the file if(argc >= 2) { const char * overrideTexName = NULL; if(argc >= 3) overrideTexName = argv[2]; LoadFile(argv[1], overrideTexName); } // Enter the main loop glutMainLoop(); } catch(ctm_error &e) { SysMessageBox mb; mb.mMessageType = SysMessageBox::mtError; mb.mCaption = "Error"; mb.mText = string("OpenCTM error: ") + string(e.what()); mb.Show(); } catch(exception &e) { SysMessageBox mb; mb.mMessageType = SysMessageBox::mtError; mb.mCaption = "Error"; mb.mText = string(e.what()); mb.Show(); } cout << endl; } //----------------------------------------------------------------------------- // Bridge GLUT callback functions to class methods //----------------------------------------------------------------------------- // NOTE: This is just a hack to be able to reference the application class // object from the GLUT callback functions, since there is no way (afaik) to // pass user data (i.e. the object reference) through GLUT... static GLViewer * gGLViewer = NULL; /// Redraw function. void GLUTWindowRedraw(void) { if(gGLViewer) gGLViewer->WindowRedraw(); } /// Resize function. void GLUTWindowResize(int w, int h) { if(gGLViewer) gGLViewer->WindowResize(w, h); } /// Mouse click function void GLUTMouseClick(int button, int state, int x, int y) { if(gGLViewer) gGLViewer->MouseClick(button, state, x, y); } /// Mouse move function void GLUTMouseMove(int x, int y) { if(gGLViewer) gGLViewer->MouseMove(x, y); } /// Keyboard function void GLUTKeyDown(unsigned char key, int x, int y) { if(gGLViewer) gGLViewer->KeyDown(key, x, y); } /// Keyboard function (special keys) void GLUTSpecialKeyDown(int key, int x, int y) { if(gGLViewer) gGLViewer->SpecialKeyDown(key, x, y); } //----------------------------------------------------------------------------- // Program startup //----------------------------------------------------------------------------- /// Program entry. int main(int argc, char **argv) { // Run the application class gGLViewer = new GLViewer; gGLViewer->Run(argc, argv); delete gGLViewer; gGLViewer = NULL; return 0; }