学习如何使用纹理映射有很多的好处。比如说,你想绘制一颗导弹从屏幕上飞过。这节课之前,你可能想通过使用多边形来组成导弹,再加上一些颜色。通过使用纹理映射,你可以使用一张导弹的图片来绘制导弹,然后让这张图片飞过屏幕。你认为哪种效果会更好呢?是一张图片还是一个由一堆三角形和正方形组成的物体?通过纹理映射,你不仅可以使程序的效果更好,而且还可以使程序更加流畅。使用纹理映射的导弹,可能仅仅是一个正方形从屏幕上飞过。而一个由多边形组成的导弹,可能包含了成百上千个多边形。而一个纹理映射的正方形将消耗更少的资源。
首先,我们在第一课的代码头部添加五行新的代码。第一行是包含一个头文件。包含这行代码之后,我们就可以操作文件了。为了稍候使用 fopen()函数,我们需要引入这个头文件。然后,我们添加三个浮点型变量,xrot, yrot 和 zrot。这些变量将用来在x轴,y轴和z轴上旋转正方体。最后一个GLuint texture[1] 变量用来设置一个纹理映射的储存空间。如果你想加载多个纹理,你可以把这个数组的尺寸改为你想要加载纹理的个数。
#include <windows.h> // Windows头文件
#include <stdio.h> // 标准输入/输出头文件 ( NEW )
#include <gl\gl.h> // OpenGL32库头文件
#include <gl\glu.h> // GLu32库头文件
#include <gl\glaux.h> // GLaux库头文件
HGLRC hRC=NULL;
// 永久的渲染上下文( Rendering Context)
HDC
hDC=NULL;
// 私有的GDI设备上下文( GDI Device Context)
HWND
hWnd=NULL;
// 获得我们窗口的句柄
HINSTANCE
hInstance;
// 获得应用程序的实例
bool
keys[256];
// 用于键盘行为的数组
bool
active=TRUE;
// 窗口活动标记,默认设置为TRUE
bool
fullscreen=TRUE;
// 全屏标记,默认设置为全屏
GLfloat xrot;
// X 轴旋转角度 ( NEW )
GLfloat yrot;
// Y 轴旋转角度 ( NEW )
GLfloat zrot;
// Z 轴旋转角度 ( NEW )
GLuint texture[1];
// 纹理存储空间 ( NEW )
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);//
声明WndProc()函数
紧跟上面这行代码,在定义ReSizeGLScene()函数之前,我们加入下面这部分代码。这部分代码的作用是用来加载一个Bitmap文件。如果这个文件不存在,这个函数返回NULL,表示这个纹理无法加载。在我解释这部分代码之前,你需要知道想要把一幅图像作为纹理来使用的一些重要信息。这幅图像的宽和高必须是2的n次方。这幅图像的宽和高至少要是64像素,考虑到兼容性,宽和高最好不要大于256像素。如果你的图像不是64,128或者256像素的,通过图像软件把它调整到正确的尺寸。有一些方法可以绕过这些现在,但是现在我们将使用标准的纹理尺寸。
首先,我们创建一个文件句柄。一个句柄是一个文件标识符的值,我们的程序通过它可以访问文件。我们先把它置为NULL。
AUX_RGBImageRec *LoadBMP(char *Filename) // 加载一幅Bitmap图像
{
FILE *File=NULL; // 文件句柄
然后检查我们是否已经有了一个要加载文件的文件名。有些人可能没有确认文件是否存在就通过LoadBMP()方法来加载它,所有我们必须做这个检查。我们不想什么都不加载。
if (!Filename) // 检查是否已经有了一个要加载文件的文件名
{
return NULL; // 如果没有,返回 NULL
}
如果文件名不为空,我们检查这个文件是否存在。下面的这行代码试着打开这个文件。
File=fopen(Filename,"r"); //检查这个文件是否存在
如果我们能够打开它,说明这个文件存在。我么你使用fclose(File)函数关闭这个文件,然后返回图像数据。
auxDIBImageLoad(Filename) 函数用来读取它的数据。
if (File) // 文件是否存在
{
fclose(File); // 如果存在,关闭文件句柄
return auxDIBImageLoad(Filename); // 加载Bitmap图像,返回一个指针
}
如果不能打开这个文件,返回NULL,意味着文件不能加载。稍候,我们会在程序中检查这个文件是否成功加载。如果没有成功加载,我们退出程序,弹出一个错误消息框。
return NULL; // 如果加载失败,返回 NULL
}
下面这部分代码是加载一幅Bitmap图像(调用上面的代码),然后把它转换成一个纹理。
int LoadGLTextures() // 加载一幅Bitmap图像,然后把它转换成一个纹理
{
我们声明一个名为Stutas的变量。我们将使用这个变量来跟踪是否我们成功加载一幅Bitmap图像,并把它转换为纹理。它的默认值为FALSE(意味着还没有任何东西加载或创建)。
int Status=FALSE; // 状态标识符
现在我们创建一个图像记录(原文:image record)来处存我们的Bitmap。这个记录包含Bitmap的宽,高和数据。
AUX_RGBImageRec *TextureImage[1]; // 创建一个纹理的储存空间
然后清空这个图形记录。
memset(TextureImage,0,sizeof(void *)*1); // 设置指针指向 NULL
接下来,我们加载Bitmap图像,并把它转换成纹理。TextureImage[0]=LoadBMP("Data/NeHe.bmp") 将会跳转到我们的LoadBMP()这部分代码。在Data文件夹中文件名为NeHe.bmp的文件被加载进来。如果一切顺利,图像数据会保存在TextureImage[0]中,Status变量设置为TRUE,然后我们创建我们的纹理。
//加载Bitmap,查错,如果加载失败,退出程序
if (TextureImage[0]=LoadBMP("Data/NeHe.bmp"))
{
Status=TRUE; // Status变量设置为TRUE
现在我们已经把图像数据加载到了TextureImage[0]中,我们将使用这个数据创建一个纹理。第一行glGenTextures(1, &texture[0])函数通知OpenGL我们要生成一个纹理。(如果你想加载多个纹理,把1改为你想要加载纹理的个数)还记得我们在这节课的开始设置GLuint texture[1]变量来存储纹理吧。尽管你可能认为第一个纹理应该被存储在&texture[1]而不是&texture[0]中,但是,并不是这样的。第一个纹理实际上被存储在
&texture[0]
中。如果你想加载两个纹理,我们使用GLuint texture[2]
,第二个纹理被加载到&texture[1]
中。
第二行代码
glBindTexture(GL_TEXTURE_2D, texture[0])通知OpenGL绑定texture[0]
中的数据。2D的纹理有宽(x轴)和高(y轴)。glBindTexture函数分配一个纹理名称给纹理数据。本例中我们告知OpenGL, &texture[0] 处的内存已经可用。我们创建的纹理将存储在&texture[0]指向的内存区域。
glGenTextures(1, &texture[0]); // 创建纹理
// 普通的纹理生成使用Bitmap图像数据
glBindTexture(GL_TEXTURE_2D, texture[0]);
下面,我们建立实际的纹理。下面的代码告诉OpenGL我们生成的是2D的纹理(GL_TEXTURE_2D)。0代表图像细节等级(images level of detail),通常设置为0。3是数据成分的个数。因为图像是由红色数据,绿色数据和蓝色数据,三种成分组成的。TextureImage[0]->sizeX是纹理的宽度。如果你知道纹理的宽度,可以在这里设置,当然,让计算机为你计算出来会更加容易。TextureImage[0]->sizey是纹理的高度。0是边界的宽度。通常把它设置为0。GL_RGB告诉OpenGL我们使用的图像数据是按照红色数据,绿色数据和蓝色数据的顺序组成的。GL_UNSIGNED_BYTE是说我们的图像数据是unsigned byte类型的。最后,TextureImage[0]->data告诉OpenGL纹理数据存储在哪里。这里,它指向TextureImage[0]记录里存储的数据。
//生成纹理
glTexImage2D(GL_TEXTURE_2D, 0, 3, TextureImage[0]->sizeX, TextureImage[0]->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, TextureImage[0]->data);
下面的两行代码告诉OpenGL当绘制的图像比原始的纹理大( GL_TEXTURE_MAG_FILTER )或者在屏幕上被拉伸,或者比原始的纹理小( GL_TEXTURE_MIN_FILTER )时,OpenGL采用的滤波方式。这两种情况我通常都使用GL_LINEAR。这样可以使纹理在屏幕的远处和近处看上去都很平滑。使用GL_LINEAR需要高性能的CPU和显卡,所以如果你的机器很慢,你应该使用GL_NEAREST。使用GL_NEAREST滤波的纹理当它被拉伸时会出现失真。你也可以结合这两种情况,在近处时使用滤波效果,在远处时不使用滤波效果。
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR); //线性滤波
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR); //线性滤波
}
然后我们释放用来存储纹理数据的内存。我们检查纹理数据是否存储在TextureImage[0]中。然后我们检查数据是否已经存储了。如果数据已经存储了,我们释放它。然后我们释放这个图像结构体,确定用来存储纹理数据的内存都释放掉。
if (TextureImage[0]) // 如果纹理存在
{
if (TextureImage[0]->data) //如果纹理图像存在
{
free(TextureImage[0]->data); // 释放纹理图像内存
}
free(TextureImage[0]); // 释放图像结构体内存
}
最后,我们返回Status变量。如果一切顺利,这个变量应该为TRUE。如果在某个地方出错了,这个变量为FALSE。
return Status; // 返回Status变量
}
我在InitGL函数中添加了一些新的代码。我重写了全部的代码,所以你可以很清楚的看到我在哪里添加了新的代码。第一行代码未if (!LoadGLTextures()) 跳转到上面加载Bitmap的那部分代码,然后生成一个纹理。如果LoadGLTextures()
出错了,返回FALSE。如果一切顺利,纹理正确地创建了,我们启用2D纹理映射。如果你忘记了启用纹理映射,你绘制的将是一个效果很差的实心白色物体。
int InitGL(GLvoid)//
在这里做
所有有关OpenGL的设置
{
if
(!LoadGLTextures())
// 跳转到加载纹理的部分 ( NEW )
{
return
FALSE;
// 如果纹理不能成功加载,返回 FALSE ( NEW )
}
glEnable(GL_TEXTURE_2D);
//启用纹理映射 ( NEW )
glShadeModel(GL_SMOOTH);
//启用平滑阴影
glClearColor(0.0f, 0.0f, 0.0f, 0.5f);
//黑色背景
glClearDepth(1.0f);
//设置深度缓存
glEnable(GL_DEPTH_TEST);
//启用深度测试
glDepthFunc(GL_LEQUAL);
//深度测试的类型
glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);
//出色的透视计算
return TRUE;
//初始化成功
}
int DrawGLScene(GLvoid)
//我们绘制图形的地方
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
//清除颜色和深度缓存
glLoadIdentity();
//重置模型视图矩阵
glTranslatef(0.0f,0.0f,-5.0f);
// 移入屏幕5个单位
接下来的三行代码将会在x轴,y轴和z轴上旋转正方体。在每个轴上的旋转角度保存在xrot, yrot 和 zrot变量中。
glRotatef(xrot,1.0f,0.0f,0.0f); // 在X 轴上旋转
glRotatef(yrot,0.0f,1.0f,0.0f); // 在Y轴上旋转
glRotatef(zrot,0.0f,0.0f,1.0f); // 在Z 轴上旋转
下面这行代码用来选择我们想使用的纹理。如果你想在你的场景中使用多个纹理,你可以使用glBindTexture(GL_TEXTURE_2D, texture[number of texture to use])来选择纹理。如果你想改变纹理,你应该重新绑定纹理。有一件事需要注意,你不能在glBegin() 和 glEnd()之间绑定纹理,你必须在它们之前或之后绑定纹理。注意我们是如何使用glBindTextures来明确地创建一个纹理,以及如何选择一个明确的纹理。
glBindTexture(GL_TEXTURE_2D, texture[0]); // 选择纹理
为了正确地把纹理映射到正方形上,你必须确定纹理的右上角映射到正方形的右上角,纹理的左上角映射到正方形的左上角,纹理的右下角映射到正方形的右下角,纹理的左下角映射到正方形的左下角。如果纹理的各个顶角没有映射到正方形相同的顶角上,图像显示时可能上下颠倒,或侧向一边,或什么都没有。
glTexCoord2f的第一个参数值是x坐标。0.0是纹理的左边,0.5是纹理的中间,1.0是纹理的右边。glTexCoord2f的第二个参数值是y坐标。0.0是纹理的底部,0.5是纹理的中间,1.0是纹理的顶部。
所以现在我们知道了纹理的左上角坐标是(0.0f,1.0f),正方形的左上顶点坐标是(-1.0f,1.0f)。现在,你把其它的三个纹理顶点坐标对应到正方形剩余的三个顶点上。
现在尝试改变glTexCoord2f参数x的值。把1.0改为0.5,将会只绘制从0.0(左边)到0.5(中间)的左半边的纹理。将0.0改为0.5,将会只绘制从0.5(中间)到1.0(右边)的右半边的纹理。
glBegin(GL_QUADS);
// 正面
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // 左下角
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // 右下角
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // 右上角
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // 左上角
// 背面
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // 右下角
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // 右上角
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // 左上角
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // 左下角
// 上面
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // 左上角
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // 左下角
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // 右下角
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // 右上角
// 底面
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // 右上角
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // 左上角
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // 左下角
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // 右下角
// 右面
glTexCoord2f(1.0f, 0.0f); glVertex3f( 1.0f, -1.0f, -1.0f); // 右下角
glTexCoord2f(1.0f, 1.0f); glVertex3f( 1.0f, 1.0f, -1.0f); // 右上角
glTexCoord2f(0.0f, 1.0f); glVertex3f( 1.0f, 1.0f, 1.0f); // 左上角
glTexCoord2f(0.0f, 0.0f); glVertex3f( 1.0f, -1.0f, 1.0f); // 左下角
// 左面
glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, -1.0f); // 左下角
glTexCoord2f(1.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 1.0f); // 右下角
glTexCoord2f(1.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 1.0f); // 右上角
glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, -1.0f); // 左上角
glEnd();
然后我们增加xrot, yrot 和 zrot变量的值。尝试改变每个变量每次增加的值,来使正方体旋转的更快或更慢,或者把+号改为-号,使正方体朝相反的方向旋转。
xrot+=0.3f; // X 轴旋转
yrot+=0.2f; // Y 轴旋转
zrot+=0.4f; // Z 轴旋转
return true;
}
现在你应该很好地理解了什么是纹理映射。。您应该可以给任意四边形表面贴上您所喜爱的图像。一旦您觉得你已经熟练掌握了2D纹理映射,试试给立方体的六个面贴上不同的纹理。