NeHe OpenGL教程 第六课 纹理映射

学习如何使用纹理映射有很多的好处。比如说,你想绘制一颗导弹从屏幕上飞过。这节课之前,你可能想通过使用多边形来组成导弹,再加上一些颜色。通过使用纹理映射,你可以使用一张导弹的图片来绘制导弹,然后让这张图片飞过屏幕。你认为哪种效果会更好呢?是一张图片还是一个由一堆三角形和正方形组成的物体?通过纹理映射,你不仅可以使程序的效果更好,而且还可以使程序更加流畅。使用纹理映射的导弹,可能仅仅是一个正方形从屏幕上飞过。而一个由多边形组成的导弹,可能包含了成百上千个多边形。而一个纹理映射的正方形将消耗更少的资源。

首先,我们在第一课的代码头部添加五行新的代码。第一行是包含一个头文件。包含这行代码之后,我们就可以操作文件了。为了稍候使用 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; //初始化成功

}
然后,我们绘制带有纹理映射的正方体。你可以直接使用下面的DrawGLScene函数的代码,或者在第一课中的DrawGLScene函数中添加一些新的代码。这部分代码的注释很详细,所以很容易理解。前两行代码是第一课中的代码。glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 将会使用InitGL()函数中我们设置的颜色来清理屏幕。这里,屏幕将被清理为黑色。深度缓存也会清理。glLoadIdentity()函数重置视图窗口。
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纹理映射,试试给立方体的六个面贴上不同的纹理。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值