学习纹理贴图是十分有用的。 假如我们想使一个导弹飞过屏幕, 用前面的知识我们大概只能用多边形来构造。 而使用纹理映射, 我们完全可以让一个真实的导弹图片飞过屏幕。 一张照片和一个用多边形构造的物体, 你认为哪一个会更好呢? 使用纹理贴图, 不仅能得到更好的视觉效果, 而且还能得到更好的运行效率。 因为使用纹理贴图制作的导弹可以仅仅是一个四边形, 而如果我们完全使用多边形来构造的话, 就有可能需要成百上千的多边形了。 所以使用纹理映射的四边形将为我们节省大量的运算。
我们以第一篇教程的代码作为基础, 首先在开始处增加5行新的代码。 第一行新代码是 #include <stdio.h>, 因为我们之后要使用fopen()。 还有三行新代码是增加了三个浮点变量: xrot, yrot 和 zrot, 它们分别用于控制立方体在x,y和z轴上的旋转。 最后一行新代码是 GLuint texture[1], 用于存储一个纹理对象(译注:纹理对象是一个非零的无符号整数)。 如果有更多的纹理对象要保存, 就需要修改数组元素的数目为相应的纹理对象的数目。
#include <windows.h> // Header File For Windows
#include <stdio.h> // Header File For Standard Input/Output ( NEW )
#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
bool fullscreen=TRUE; // Fullscreen Flag
GLfloat xrot; // X Rotation ( NEW )
GLfloat yrot; // Y Rotation ( NEW )
GLfloat zrot; // Z Rotation ( NEW )
GLuint texture[1]; // Storage For One Texture ( NEW )
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); // Declaration For WndProc
下面我们增加了一段新的代码, 其作用是读入位图文件。 如果文件不存在的话, 将返回NULL值代表文件不能被读取。 在我解释代码之前, 还要讲一些关于纹理图像的很重要的事情: 纹理图像的宽度和高度必须是2的幂。 宽度或高度最小应是64个像素, 为了兼容性,最多应是256个像素。 如果图像的宽度或高度不是64,128或256个像素, 就应该重新调整图像尺寸, 这是解决这一限制的方法。 但现在我们只使用标准的纹理尺寸。
(译注: 纹理的最小尺寸限制和最大尺寸限制其实取决于具体的OpenGL实现)
首先我们创建一个文件句柄, 并初始化为 NULL。
AUX_RGBImageRec *LoadBMP(char *Filename) // Loads A Bitmap Image
{
FILE *File=NULL; // File Handle
接下来我们检测文件名是否合法, 因为使用LoadBMP() 的用户有可能根本没有给出文件名。
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
如果文件能打开证明其一定存在。 我们关闭文件, 然后返回 auxDIBImageLoad(Filename) 读入的图像数据。
if (File) // Does The File Exist?
{
fclose(File); // Close The Handle
return auxDIBImageLoad(Filename); // Load The Bitmap And Return A Pointer
}
如果不能打开文件, 我们返回 NULL。 之后在程序中我们要检测文件是否读取了, 如果没有的话, 我们将退出程序并给出一个错误消息。
return NULL; // If Load Failed Return NULL
}
下面这段代码的作用是读取位图 (通过调用上面的代码) 并转换成纹理。
int LoadGLTextures() // Load Bitmaps And Convert To Textures
{
我们设置一个 Status 变量, 它用于记录我们是否成功地读取了位图并建造了纹理。 我们将其初始化为FLASE (代表什么都没有读取和建造)。
int Status=FALSE; // Status Indicator
现在我们创建一个用于保存位图的图像记录, 它将持有图像的宽度, 高度和数据。
AUX_RGBImageRec *TextureImage[1]; // Create Storage Space For The Texture
首先清空图像记录。
memset(TextureImage,0,sizeof(void *)*1); // Set The Pointer To NULL
现在我们读入位图。 TextureImage[0]=LoadBMP("Data/NeHe.bmp") 将调用上面的LoadBMP(), 读入 Data 目录中的 NeHe.bmp。 如果顺利, 图像数据会保存在TextureImage[0] 中, Status 被设置为 TRUE, 然后我们开始建造纹理。
// Load The Bitmap, Check For Errors, If Bitmap's Not Found Quit
if (TextureImage[0]=LoadBMP("Data/NeHe.bmp"))
{
Status=TRUE; // Set The Status To TRUE
现在我们已经把图像数据读入了 TextureImage[0], 我们将用这个数据来建造纹理。 下面的第一行代码glGenTextures(1, &texture[0]) 用于获得一个未使用的纹理名称。
第二行代码glBindTexture(GL_TEXTURE_2D, texture[0]) 绑定一个纹理名称到一个纹理对象。
(译注:glBindTexture 即创建纹理对象, 又绑定纹理对象, 又使用纹理对象。 详细内容请看OpenGL红皮书)
glGenTextures(1, &texture[0]); // Create The Texture
// Typical Texture Generation Using Data From The Bitmap
glBindTexture(GL_TEXTURE_2D, texture[0]);
现在我们建造实际的纹理(译注:这一步是指定纹理数据)。 下面这行代码告知OpenGL 我们将定义一个2D纹理。 0代表图像的详细级别, 这通常为0 (译注:这与多纹理贴图有关)。 3 指定数据成分, 因为我们的图像是红,绿,蓝组成的, 所以为3。 TextureImage[0]->sizeX 是纹理宽度, 如果你知道具体的宽度数值, 可以直接写在这儿, 不过我们这样做更简单。 TextureImage[0]->sizeY 是纹理高度。 0 是纹理边界, 通常为 0。 GL_RGB 告知 OpenGL 我们的图像数据是红,绿,蓝顺序存储的。GL_UNSIGNED_BYTE 表示图像的数据类型是8位无符号整数。 最后TextureImage[0]->data 指定纹理数据, 在这里指向TextureImage[0] 中保存的数据。
(译注:详细内容请看OpenGL红皮书)
// Generate The Texture
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
下面两行代码分别用于设置在放大 (GL_TEXTURE_MAG_FILTER) 和缩小 (GL_TEXTURE_MIN_FILTER) 纹理贴图的时候所使用的过滤。 我通常将它们都设置为GL_LINEAR, 这使纹理贴图在距离屏幕很远和很近的时候都能看起来很平滑。 当然, 使用 GL_LINEAR 是要牺牲一些效率的, 所以如果你的系统较慢, 你可以使用 GL_NEAREST, 不过使用 GL_NEAREST 可能会出现一些锯齿。 当然你可以把它们结合起来使用, 放大用一种, 缩小用一种。
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);// Linear Filtering
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);// Linear Filtering
}
现在我们释放掉存储位图数据的所有内存。 首先检查是否有位图保存在TextureImage[0], 有的话检查是否有数据, 有即释放掉, 最后释放掉图像结构, 确保所有内存都释放干净了。
if (TextureImage[0]) // If Texture Exists
{
if (TextureImage[0]->data) // If Texture Image Exists
{
free(TextureImage[0]->data); // Free The Texture Image Memory
}
free(TextureImage[0]); // Free The Image Structure
}
最后返回状态。 如果一切顺利, 变量Status 将为 TRUE; 否则为 FALSE。
return Status; // Return The Status
}
在InitGL中我添加了几行新的代码。 第一行 if (!LoadGLTextures()) 调用上面的代码, 读取位图并建造纹理。 如果由于一些原因LoadGLTextures() 失败了, 那么下面一行代码将返回 FALSE。 如果读入纹理成功, 那么我们就启用(激活)纹理映射。 如果你忘记了启用纹理映射, 物体看起来将是白色的。
int InitGL(GLvoid) // All Setup For OpenGL Goes Here
{
if (!LoadGLTextures()) // Jump To Texture Loading Routine ( NEW )
{
return FALSE; // If Texture Didn't Load Return FALSE ( NEW )
}
glEnable(GL_TEXTURE_2D); // Enable Texture Mapping ( NEW )
glShadeModel(GL_SMOOTH); // Enable Smooth Shading
glClearColor(0.0f, 0.0f, 0.0f, 0.5f); // Black Background
glClearDepth(1.0f); // Depth Buffer Setup
glEnable(GL_DEPTH_TEST); // Enables Depth Testing
glDepthFunc(GL_LEQUAL); // The Type Of Depth Testing To Do
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Really Nice Perspective Calculations
return TRUE; // Initialization Went OK
}
现在我们绘制贴图立方体。 头两行代码都是原来的代码, glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 用于清除屏幕为指定颜色和清除深度缓冲区, 在这个例子中屏幕将被清除为黑色。 glLoadIdentity() 重置视图(译注:模型视图矩阵)。
int DrawGLScene(GLvoid) // Here's Where We Do All The Drawing
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer
glLoadIdentity(); // Reset The Current Matrix
glTranslatef(0.0f,0.0f,-5.0f); // Move Into The Screen 5 Units
接下来的3行代码用于在x,y和z轴上旋转立方体, 旋转的角度有分别由变量xrot,yrot 和 zrot 控制。
glRotatef(xrot,1.0f,0.0f,0.0f); // Rotate On The X Axis
glRotatef(yrot,0.0f,1.0f,0.0f); // Rotate On The Y Axis
glRotatef(zrot,0.0f,0.0f,1.0f); // Rotate On The Z Axis
下面一行代码用于选择我们要使用的纹理。 如果想变换纹理, 就需要绑定另一个纹理。 有一点很重要, 那就是绑定纹理操作不能在glBegin() 和 glEnd() 之间进行。 请注意我们是如何使用glBindTextures 来创建和选择一个指定的纹理的。
glBindTexture(GL_TEXTURE_2D, texture[0]); // Select Our Texture
想要适当地映射一个纹理(贴图)到一个四边形上, 你必须确保它们的右上角,左上角,右下角和左下角都能正确地一一对应, 否则纹理有可能被倒置过来, 也有可能歪斜了, 或者根本没贴上。
glTexCoord2f 的第一个参数是x坐标(译注:纹理坐标。为了与物体坐标相区分,纹理坐标一般使用字母s和t代替x和y), 0.0f 就是纹理的左边, 0.5f 就是纹理的中间, 1.0f 就是纹理的右边。glTexCoord2f 的第二个参数是y坐标(译注:纹理坐标), 0.0f 就是纹理的底边, 0.5f 是中间, 而1.0f 就是纹理的顶边(最上边)。
(译注:纹理坐标范围是0到1,详细请看红皮书)
现在我们知道, 纹理左上角的坐标是0.0f, 1.0f, 而四边形左上角的顶点坐标是 -1.0f, 1.0f。 然后我们要做的就是匹配四边形剩下的3个角。
你可以试着修改glTexCoord2f 的参数, 把1.0f 改为 0.5f 只绘制纹理的左半部分(0.0f至0.5f), 把 0.0f 改为 0.5f 只绘制它的右半部分(0.5f至1.0f)。
glBegin(GL_QUADS);
// Front Face
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Bottom Left Of The Texture and Quad
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Top Left Of The Texture and Quad
// Back Face
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Top Left Of The Texture and Quad
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Bottom Left Of The Texture and Quad
// Top Face
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Top Left Of The Texture and Quad
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Bottom Left Of The Texture and Quad
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Top Right Of The Texture and Quad
// Bottom Face
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Top Left Of The Texture and Quad
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Bottom Left Of The Texture and Quad
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Bottom Right Of The Texture and Quad
// Right face
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // Top Left Of The Texture and Quad
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // Bottom Left Of The Texture and Quad
// Left Face
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // Bottom Left Of The Texture and Quad
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // Bottom Right Of The Texture and Quad
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // Top Right Of The Texture and Quad
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // Top Left Of The Texture and Quad
glEnd();
现在我们给xrot,yrot 和 zrot 增量。 你可以试着修改这些增量值, 让立方体旋转的更快或者更慢。 或者, 把+改为-,让其向反方向旋转。
xrot+=0.3f; // X Axis Rotation
yrot+=0.2f; // Y Axis Rotation
zrot+=0.4f; // Z Axis Rotation
return true; // Keep Going
}
你应该已经较好地理解了纹理映射, 可以给自己的四边形贴图了。 如果你对2D纹理映射感觉有了信心, 你可以试着给立方体的6个面映射不同的纹理贴图。
如果你理解了纹理坐标, 那么纹理映射是不难理解的。 如果此篇教程的某些地方你不明白, 告诉我, 我会重写那一部分教程,或者,在email里给你答复。 祝你贴图愉快:)
Jeff Molofee (NeHe)
(译著) 作者在纹理坐标这个关键的地方并没有做多少叙述,但是由于这是非常基础的,所以并无大碍。 如果你发现了什么问题或者疏漏, 请即时反馈给我, 这样我就能做出相应的补救或者更正。 十分欢迎你的支持和鼓励, 那将会使我更有动力。