-- 作者:一分之千
-- 发布时间:10/18/2007 5:37:00 PM
--
Lesson 17 This tutorial brought to you by NeHe & Giuseppe D'Agata... I know everyones probably sick of fonts. The text tutorials I've done so far not only display text, they display 3D text, texture mapped text, and can handle variables. But what happens if you're porting your project to a machine that doesn't support Bitmap or Outline fonts? Thanks to Giuseppe D'Agata we have yet another font tutorial. What could possibly be left you ask!? If you remember in the first Font tutorial I mentioned using textures to draw letters to the screen. Usually when you use textures to draw text to the screen you load up your favorite art program, select a font, then type the letters or phase you want to display. You then save the bitmap and load it into your program as a texture. Not very efficient for a program that require alot of text, or text that continually changes! This program uses just ONE texture to display any of 256 different characters on the screen. Keep in mind your average character is just 16 pixels wide and roughly 16 pixels tall. If you take your standard 256x256 texture it's easy to see that you can fit 16 letters across, and you can have a total of 16 rows up and down. If you need a more detailed explanation: The texture is 256 pixels wide, a character is 16 pixels wide. 256 divided by 16 is 16 :) So... Lets create a 2D textured font demo! This program expands on the code from lesson 1. In the first section of the program, we include the math and stdio libraries. We need the math library to move our letters around the screen using SIN and COS, and we need the stdio library to make sure the bitmaps we want to use actually exist before we try to make textures out of them. #include <windows.h> // Header File For Windows #include <math.h> // Header File For Windows Math Library ( ADD ) #include <stdio.h> // Header File For Standard Input/Output ( ADD ) #include <gl\gl.h> // Header File For The OpenGL32 Library #include <gl\glu.h> // Header File For The GLu32 Library #include <gl\glaux.h> // Header File For The GLaux Library HDC hDC=NULL; // Private GDI Device Context HGLRC hRC=NULL; // Permanent Rendering Context HWND hWnd=NULL; // Holds Our Window Handle HINSTANCE hInstance; // Holds The Instance Of The Application bool keys[256]; // Array Used For The Keyboard Routine bool active=TRUE; // Window Active Flag Set To TRUE By Default bool fullscreen=TRUE; // Fullscreen Flag Set To Fullscreen Mode By Default We're going to add a variable called base to point us to our display lists. We'll also add texture[2] to hold the two textures we're going to create. Texture 1 will be the font texture, and texture 2 will be a bump texture used to create our simple 3D object. We add the variable loop which we will use to execute loops. Finally we add cnt1 and cnt2 which we will use to move the text around the screen and to spin our simple 3D object. GLuint base; // Base Display List For The Font GLuint texture[2]; // Storage For Our Font Texture GLuint loop; // Generic Loop Variable GLfloat cnt1; // 1st Counter Used To Move Text & For Coloring GLfloat cnt2; // 2nd Counter Used To Move Text & For Coloring LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc Now for the texture loading code. It's exactly the same as it was in the previous texture mapping tutorials. AUX_RGBImageRec *LoadBMP(char *Filename) // Loads A Bitmap Image { FILE *File=NULL; // File Handle if (!Filename) // Make Sure A Filename Was Given { return NULL; // If Not Return NULL } File=fopen(Filename,"r"); // Check To See If The File Exists if (File) // Does The File Exist? { fclose(File); // Close The Handle return auxDIBImageLoad(Filename); // Load The Bitmap And Return A Pointer } return NULL; // If Load Failed Return NULL } The follwing code has also changed very little from the code used in previous tutorials. If you're not sure what each of the following lines do, go back and review. Note that TextureImage[ ] is going to hold 2 rgb image records. It's very important to double check code that deals with loading or storing our textures. One wrong number could result in a memory leak or crash! int LoadGLTextures() // Load Bitmaps And Convert To Textures { int Status=FALSE; // Status Indicator AUX_RGBImageRec *TextureImage[2]; // Create Storage Space For The Textures The next line is the most important line to watch. If you were to replace the 2 with any other number, major problems will happen. Double check! This number should match the number you used when you set up TextureImages[ ]. The two textures we're going to load are font.bmp (our font), and bumps.bmp. The second texture can be replaced with any texture you want. I wasn't feeling very creative, so the texture I decided to use may be a little drab. memset(TextureImage,0,sizeof(void *)*2); // Set The Pointer To NULL if ((TextureImage[0]=LoadBMP("Data/Font.bmp")) && // Load The Font Bitmap (TextureImage[1]=LoadBMP("Data/Bumps.bmp"))) // Load The Texture Bitmap { Status=TRUE; // Set The Status To TRUE Another important line to double check. I can't begin to tell you how many emails I've received from people asking "why am I only seeing one texture, or why are my textures all white!?!". Usually this line is the problem. If you were to replace the 2 with a 1, only one texture would be created and the second texture would appear all white. If you replaced the 2 with a 3 you're program may crash! You should only have to call glGenTextures() once. After glGenTextures() you should generate all your textures. I've seen people put a glGenTextures() line before each texture they create. Usually they causes the new texture to overwrite any textures you've already created. It's a good idea to decide how many textures you need to build, call glGenTextures() once, and then build all the textures. It's not wise to put glGenTextures() inside a loop unless you have a reason to. glGenTextures(2, &texture[0]); // Create Two Texture for (loop=0; loop<2; loop++) // Loop Through All The Textures { // Build All The Textures glBindTexture(GL_TEXTURE_2D, texture[loop]); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[loop]->sizeX, TextureImage[loop]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[loop]->data); } } The following lines of code check to see if the bitmap data we loaded to build our textures is using up ram. If it is, the ram is freed. Notice we check and free both rgb image records. If we used 3 different images to build our textures, we'd check and free 3 rgb image records. for (loop=0; loop<2; loop++) { if (TextureImage[loop]) // If Texture Exists { if (TextureImage[loop]->data) // If Texture Image Exists { free(TextureImage[loop]->data); // Free The Texture Image Memory } free(TextureImage[loop]); // Free The Image Structure } } return Status; // Return The Status } Now we're going to build our actual font. I'll go through this section of code in some detail. It's not really that complex, but there's a bit of math to understand, and I know math isn't something everyone enjoys. GLvoid BuildFont(GLvoid) // Build Our Font Display List { The following two variable will be used to hold the position of each letter inside the font texture. cx will hold the position from left to right inside the texture, and cy will hold the position up and down. float cx; // Holds Our X Character Coord float cy; // Holds Our Y Character Coord Next we tell OpenGL we want to build 256 display lists. The variable base will point to the location of the first display list. The second display list will be base+1, the third will be base+2, etc. The second line of code below selects our font texture (texture[0]). base=glGenLists(256); // Creating 256 Display Lists glBindTexture(GL_TEXTURE_2D, texture[0]); // Select Our Font Texture Now we start our loop. The loop will build all 256 characters, storing each character in it's own display lists. for (loop=0; loop<256; loop++) // Loop Through All 256 Lists { The first line below may look a little puzzling. The % symbol means the remainder after loop is divided by 16. cx will move us through the font texture from left to right. You'll notice later in the code we subtract cy from 1 to move us from top to bottom instead of bottom to top. The % symbol is fairly hard to explain but I will make an attempt. All we are really concerned about is (loop%16) the /16.0f just converts the results into texture coordinates. So if loop was equal to 16... cx would equal the remained of 16/16 which would be 0. but cy would equal 16/16 which is 1. So we'd move down the height of one character, and we wouldn't move to the right at all. Now if loop was equal to 17, cx would be equal to 17/16 which would be 1.0625. The remainder .0625 is also equal to 1/16th. Meaning we'd move 1 character to the right. cy would still be equal to 1 because we are only concerned with the number to the left of the decimal. 18/16 would gives us 2 over 16 moving us 2 characters to the right, and still one character down. If loop was 32, cx would once again equal 0, because there is no remained when you divide 32 by 16, but cy would equal 2. Because the number to the left of the decimal would now be 2, moving us down 2 characters from the top of our font texture. Does that make sense? cx=float(loop%16)/16.0f; // X Position Of Current Character cy=float(loop/16)/16.0f; // Y Position Of Current Character Whew :) Ok. So now we build our 2D font by selecting an individual character from our font texture depending on the value of cx and cy. In the line below we add loop to the value of base if we didn't, every letter would be built in the first display list. We definitely don't want that to happen so by adding loop to base, each character we create is stored in the next available display list. glNewList(base+loop,GL_COMPILE); // Start Building A List Now that we've selected the display list we want to build, we create our character. This is done by drawing a quad, and then texturing it with just a single character from the font texture. glBegin(GL_QUADS); // Use A Quad For Each Character cx and cy should be holding a very tiny floating point value from 0.0f to 1.0f. If both cx and cy were equal to 0 the first line of code below would actually be: glTexCoord2f(0.0f,1-0.0f-0.0625f). Remember that 0.0625 is exactly 1/16th of our texture, or the width / height of one character. The texture coordinate below would be the bottom left point of our texture. Notice we are using glVertex2i(x,y) instead of glVertex3f(x,y,z). Our font is a 2D font, so we don't need the z value. Because we are using an Ortho screen, we don't have to translate into the screen. All you have to do to draw to an Ortho screen is specify an x and y coordinate. Because our screen is in pixels from 0 to 639 and 0 to 479, we don't have to use floating point or negative values either :) The way we set up our Ortho screen, (0,0) will be at the bottom left of our screen. (640,480) will be the top right of the screen. 0 is the left side of the screen on the x axis, 639 is the right side of the screen on the x axis. 0 is the bottom of the screen on the y axis and 479 is the top of the screen on the y axis. Basically we've gotten rid of negative coordinates. This is also handy for people that don't care about perspective and prefer to work with pixels rather than units :) glTexCoord2f(cx,1-cy-0.0625f); // Texture Coord (Bottom Left) glVertex2i(0,0); // Vertex Coord (Bottom Left) The next texture coordinate is now 1/16th to the right of the last texture coordinate (exactly one character wide). So this would be the bottom right texture point. glTexCoord2f(cx+0.0625f,1-cy-0.0625f); // Texture Coord (Bottom Right) glVertex2i(16,0); // Vertex Coord (Bottom Right) The third texture coordinate stays at the far right of our character, but moves up 1/16th of our texture (exactly the height of one character). This will be the top right point of an individual character. glTexCoord2f(cx+0.0625f,1-cy); // Texture Coord (Top Right) glVertex2i(16,16); // Vertex Coord (Top Right) Finally we move left to set our last texture coordinate at the top left of our character. glTexCoord2f(cx,1-cy); // Texture Coord (Top Left) glVertex2i(0,16); // Vertex Coord (Top Left) glEnd(); // Done Building Our Quad (Character) Finally, we translate 10 pixels to the right, placing us to the right of our texture. If we didn't translate, the letters would all be drawn on top of eachother. Because our font is so narrow, we don't want to move 16 pixels to the right. If we did, there would be big spaces between each letter. Moving by just 10 pixels eliminates the spaces. glTranslated(10,0,0); // Move To The Right Of The Character glEndList(); // Done Building The Display List } // Loop Until All 256 Are Built } The following section of code is the same code we used in our other font tutorials to free the display list before our program quits. All 256 display lists starting at base will be deleted. (good thing to do!). GLvoid KillFont(GLvoid) // Delete The Font From Memory { glDeleteLists(base,256); // Delete All 256 Display Lists } The next section of code is where all of our drawing is done. Everything is fairly new so I'll try to explain each line in great detail. Just a small note: Alot can be added to this code, such as variable support, character sizing, spacing, and alot of checking to restore things to how they were before we decided to print. glPrint() takes three parameters. The first is the x position on the screen (the position from left to right). Next is the y position on the screen (up and down... 0 at the bottom, bigger numbers at the top). Then we have our actual string (the text we want to print), and finally a variable called set. If you have a look at the bitmap that Giuseppe D'Agata has made, you'll notice there are two different character sets. The first character set is normal, and the second character set is italicized. If set is 0, the first character set is selected. If set is 1 or greater the second character set is selected. GLvoid glPrint(GLint x, GLint y, char *string, int set) // Where The Printing Happens { The first thing we do is make sure that set is either 0 or 1. If set is greater than 1, we'll make it equal to 1. if (set>1) // Is set Greater Than One? { set=1; // If So, Make Set Equal One } Now we select our Font texture. We do this just in case a different texture was selected before we decided to print something to the screen. glBindTexture(GL_TEXTURE_2D, texture[0]); // Select Our Font Texture Now we disable depth testing. The reason I do this is so that blending works nicely. If you don't disable depth testing, the text may end up going behind something, or blending may not look right. If you have no plan to blend the text onto the screen (so that black spaces do not show up around our letters) you can leave depth testing on. glDisable(GL_DEPTH_TEST); // Disables Depth Testing The next few lines are VERY important! We select our Projection Matrix. Right after that, we use a command called glPushMatrix(). glPushMatrix stores the current matrix (projection). Kind of like the memory button on a calculator. glMatrixMode(GL_PROJECTION); // Select The Projection Matrix glPushMatrix(); // Store The Projection Matrix Now that our projection matrix has been stored, we reset the matrix and set up our Ortho screen. The first and third numbers (0) represent the bottom left of the screen. We could make the left side of the screen equal -640 if we want, but why would we work with negatives if we don't need to. The second and fourth numbers represent the top right of the screen. It's wise to set these values to match the resolution you are currently in. There is no depth so we set the z values to -1 & 1. glLoadIdentity(); // Reset The Projection Matrix glOrtho(0,640,0,480,-1,1); // Set Up An Ortho Screen Now we select our modelview matrix, and store it's current settings using glPushMatrix(). We then reset the modelview matrix so we can work with it using our Ortho view. glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix glPushMatrix(); // Store The Modelview Matrix glLoadIdentity(); // Reset The Modelview Matrix With our perspective settings saved, and our Ortho screen set up, we can now draw our text. We start by translating to the position on the screen that we want to draw our text at. We use glTranslated() instead of glTranslatef() because we are working with actual pixels, so floating point values are not important. After all, you can't have half a pixel :) glTranslated(x,y,0); // Position The Text (0,0 - Bottom Left) The line below will select which font set we want to use. If we want to use the second font set we add 128 to the current base display list (128 is half of our 256 characters). By adding 128 we skip over the first 128 characters. glListBase(base-32+(128*set)); // Choose The Font Set (0 or 1) Now all that's left for us to do is draw the letters to the screen. We do this exactly the same as we did in all the other font tutorials. We use glCallLists(). strlen(string) is the length of our string (how many characters we want to draw), GL_BYTE means that each character is represented by a byte (a byte is any value from 0 to 255). Finally, string holds the actual text we want to print to the screen. glCallLists(strlen(string),GL_BYTE,string); // Write The Text To The Screen All we have to do now is restore our perspective view. We select the projection matrix and use glPopMatrix() to recall the settings we previously stored with glPushMatrix(). It's important to restore things in the opposite order you stored them in. glMatrixMode(GL_PROJECTION); // Select The Projection Matrix glPopMatrix(); // Restore The Old Projection Matrix Now we select the modelview matrix, and do the same thing. We use glPopMatrix() to restore our modelview matrix to what it was before we set up our Ortho display. glMatrixMode(GL_MODELVIEW); // Select The Modelview Matrix glPopMatrix(); // Restore The Old Projection Matrix Finally, we enable depth testing. If you didn't disable depth testing in the code above, you don't need this line. glEnable(GL_DEPTH_TEST); // Enables Depth Testing } Nothing has changed in ReSizeGLScene() so we'll skip right to InitGL(). int InitGL(GLvoid) // All Setup For OpenGL Goes Here { We jump to our texture building code. If texture building fails for any reason, we return FALSE. This lets our program know that an error has occurred and the program gracefully shuts down. if (!LoadGLTextures()) // Jump To Texture Loading Routine { return FALSE; // If Texture Didn't Load Return FALSE } If there were no errors, we jump to our font building code. Not much can go wrong when building the font so we don't bother with error checking. BuildFont(); // Build The Font Now we do our normal GL setup. We set the background clear color to black, the clear depth to 1.0. We choose a depth testing mode, along with a blending mode. We enable smooth shading, and finally we enable 2D texture mapping. glClearColor(0.0f, 0.0f, 0.0f, 0.0f); // Clear The Background Color To Black glClearDepth(1.0); // Enables Clearing Of The Depth Buffer glDepthFunc(GL_LEQUAL); // The Type Of Depth Test To Do glBlendFunc(GL_SRC_ALPHA,GL_ONE); // Select The Type Of Blending glShadeModel(GL_SMOOTH); // Enables Smooth Color Shading glEnable(GL_TEXTURE_2D); // Enable 2D Texture Mapping return TRUE; // Initialization Went OK } The section of code below will create our scene. We draw the 3D object first and the text last so that the text appears on top of the 3D object, instead of the 3D object covering up the text. The reason I decide to add a 3D object is to show that both perspective and ortho modes can be used at the same time. int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear The Screen And The Depth Buffer glLoadIdentity(); // Reset The Modelview Matrix We select our bumps.bmp texture so that we can build our simple little 3D object. We move into the screen 5 units so that we can see the 3D object. We rotate on the z axis by 45 degrees. This will rotate our quad 45 degrees clockwise and makes our quad look more like a diamond than a square. glBindTexture(GL_TEXTURE_2D, texture[1]); // Select Our Second Texture glTranslatef(0.0f,0.0f,-5.0f); // Move Into The Screen 5 Units glRotatef(45.0f,0.0f,0.0f,1.0f); // Rotate On The Z Axis 45 Degrees (Clockwise) After we have done the 45 degree rotation, we spin the object on both the x axis and y axis based on the variable cnt1 times 30. This causes our object to spin around as if the diamond is spinning on a point. glRotatef(cnt1*30.0f,1.0f,1.0f,0.0f); // Rotate On The X & Y Axis By cnt1 (Left To Right) We disable blending (we want the 3D object to appear solid), and set the color to bright white. We then draw a single texture mapped quad. glDisable(GL_BLEND); // Disable Blending Before We Draw In 3D glColor3f(1.0f,1.0f,1.0f); // Bright White glBegin(GL_QUADS); // Draw Our First Texture Mapped Quad glTexCoord2d(0.0f,0.0f); // First Texture Coord glVertex2f(-1.0f, 1.0f); // First Vertex glTexCoord2d(1.0f,0.0f); // Second Texture Coord glVertex2f( 1.0f, 1.0f); // Second Vertex glTexCoord2d(1.0f,1.0f); // Third Texture Coord glVertex2f( 1.0f,-1.0f); // Third Vertex glTexCoord2d(0.0f,1.0f); // Fourth Texture Coord glVertex2f(-1.0f,-1.0f); // Fourth Vertex glEnd(); // Done Drawing The First Quad Immediately after we've drawn the first quad, we rotate 90 degrees on both the x axis and y axis. We then draw another quad. The second quad cuts through the middle of the first quad, creating a nice looking shape. glRotatef(90.0f,1.0f,1.0f,0.0f); // Rotate On The X & Y Axis By 90 Degrees (Left To Right) glBegin(GL_QUADS); // Draw Our Second Texture Mapped Quad glTexCoord2d(0.0f,0.0f); // First Texture Coord glVertex2f(-1.0f, 1.0f); // First Vertex glTexCoord2d(1.0f,0.0f); // Second Texture Coord glVertex2f( 1.0f, 1.0f); // Second Vertex glTexCoord2d(1.0f,1.0f); // Third Texture Coord glVertex2f( 1.0f,-1.0f); // Third Vertex glTexCoord2d(0.0f,1.0f); // Fourth Texture Coord glVertex2f(-1.0f,-1.0f); // Fourth Vertex glEnd(); // Done Drawing Our Second Quad After both texture mapped quads have been drawn, we enable enable blending, and draw our text. glEnable(GL_BLEND); // Enable Blending glLoadIdentity(); // Reset The View We use the same fancy coloring code from our other text tutorials. The color is changed gradually as the text moves across the screen. // Pulsing Colors Based On Text Position glColor3f(1.0f*float(cos(cnt1)),1.0f*float(sin(cnt2)),1.0f-0.5f*float(cos(cnt1+cnt2))); Then we draw our text. We still use glPrint(). The first parameter is the x position. The second parameter is the y position. The third parameter ("NeHe") is the text to write to the screen, and the last parameter is the character set to use (0 - normal, 1 - italic). As you can probably guess, we swing the text around the screen using COS and SIN, along with both counters cnt1 and cnt2. If you don't understand what SIN and COS do, go back and read the previous text tutorials. glPrint(int((280+250*cos(cnt1))),int(235+200*sin(cnt2)),"NeHe",0); // Print GL Text To The Screen glColor3f(1.0f*float(sin(cnt2)),1.0f-0.5f*float(cos(cnt1+cnt2)),1.0f*float(cos(cnt1))); glPrint(int((280+230*cos(cnt2))),int(235+200*sin(cnt1)),"OpenGL",1); // Print GL Text To The Screen We set the color to a dark blue and write the author's name at the bottom of the screen. We then write his name to the screen again using bright white letters. The white letters are a little to the right of the blue letters. This creates a shadowed look. (if blending wasn't enabled the effect wouldn't work). glColor3f(0.0f,0.0f,1.0f); // Set Color To Red glPrint(int(240+200*cos((cnt2+cnt1)/5)),2,"Giuseppe D'Agata",0); // Draw Text To The Screen glColor3f(1.0f,1.0f,1.0f); // Set Color To White glPrint(int(242+200*cos((cnt2+cnt1)/5)),2,"Giuseppe D'Agata",0); // Draw Offset Text To The Screen The last thing we do is increase both our counters at different rates. This causes the text to move, and the 3D object to spin. cnt1+=0.01f; // Increase The First Counter cnt2+=0.0081f; // Increase The Second Counter return TRUE; // Everything Went OK } The code in KillGLWindow(), CreateGLWindow() and WndProc() has not changed so we'll skip over it. int WINAPI WinMain( HINSTANCE hInstance, // Instance HINSTANCE hPrevInstance, // Previous Instance LPSTR lpCmdLine, // Command Line Parameters int nCmdShow) // Window Show State { MSG msg; // Windows Message Structure BOOL done=FALSE; // Bool Variable To Exit Loop // Ask The User Which Screen Mode They Prefer if (MessageBox(NULL,"Would You Like To Run In Fullscreen Mode?", "Start FullScreen?",MB_YESNO|MB_ICONQUESTION)==IDNO) { fullscreen=FALSE; // Windowed Mode } The title of our Window has changed. // Create Our OpenGL Window if (!CreateGLWindow("NeHe & Giuseppe D'Agata's 2D Font Tutorial",640,480,16,fullscreen)) { return 0; // Quit If Window Was Not Created } while(!done) // Loop That Runs While done=FALSE { if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Is There A Message Waiting? { if (msg.message==WM_QUIT) // Have We Received A Quit Message? { done=TRUE; // If So done=TRUE } else // If Not, Deal With Window Messages { TranslateMessage(&msg); // Translate The Message DispatchMessage(&msg); // Dispatch The Message } } else // If There Are No Messages { // Draw The Scene. Watch For ESC Key And Quit Messages From DrawGLScene() if ((active && !DrawGLScene()) || keys[VK_ESCAPE]) // Active? Was There A Quit Received? { done=TRUE; // ESC or DrawGLScene Signalled A Quit } else // Not Time To Quit, Update Screen { SwapBuffers(hDC); // Swap Buffers (Double Buffering) } } } // Shutdown The last thing to do is add KillFont() to the end of KillGLWindow() just like I'm showing below. It's important to add this line. It cleans things up before we exit our program. if (!UnregisterClass("OpenGL",hInstance)) // Are We Able To Unregister Class { MessageBox(NULL,"Could Not Unregister Class.","SHUTDOWN ERROR",MB_OK | MB_ICONINFORMATION); hInstance=NULL; // Set hInstance To NULL } KillFont(); // Destroy The Font } I think I can officially say that my site now teaches every possible way to write text to the screen {grin}. All in all, I think this is a fairly good tutorial. The code can be used on any computer that can run OpenGL, it's easy to use, and writing text to the screen using this method requires very little processing power. I'd like to thank Giuseppe D'Agata for the original version of this tutorial. I've modified it heavily, and converted it to the new base code, but without him sending me the code I probably wouldn't have written the tutorial. His version of the code had a few more options, such as spacing the characters, etc, but I make up for it with the extremely cool 3D object {grin}. I hope everyone enjoys this tutorial. If you have questions, email Giuseppe D'Agata or myself. Giuseppe D'Agata Jeff Molofee (NeHe)
|