NeHe OpenGL教程 第二十二课:凹凸映射

转自【翻译】NeHe OpenGL 教程

前言

声明,此 NeHe OpenGL教程系列文章由51博客yarin翻译(2010-08-19),本博客为转载并稍加整理与修改。对NeHe的OpenGL管线教程的编写,以及yarn的翻译整理表示感谢。

 

 

NeHe OpenGL第二十二课:凹凸映射

凹凸映射,多重纹理扩展:

这是一课高级教程,请确信你对基本知识已经非常了解了。这一课是基于第六课的代码的,它将建立一个非常酷的立体纹理效果。

这一课由Jens Schneider所写,它基本上是由第6课改写而来的,在这一课里,你将学习:
怎样控制多重纹理
怎样创建一个“假”的凹凸映射
怎样做一个标志,它看起来在你的场景上方
怎样使矩阵变化更有效率
基本的多通道渲染
因为上面提到的很多方面是高级渲染得内容,我们在讲述的时候会先说明理论,接着在分析代码 。如果你已经熟悉了这些理论,你可以跳过他们,直接看代码。当你遇到什么问题的时候,不妨回过头来看看这些理论。
最后这份代码超过了1200行,大部分我们在前面的教程中遇到过了。我不会解释每一行代码,只在重要的地方做些提示,好了,让我们开始吧。 
  
#include <string.h>       // 字符串处理函数
  
MAX_EMBOSS常量定义了突起的最大值 
  
#define MAX_EMBOSS (GLfloat)0.01f      // 定义了突起的最大值

好了,现在我们准备使用GL_ARB_multitexture这个扩展,它非常简单。
大部分图形卡不止一个纹理单元,为了利用这个功能,你必须检查GL_ARB_multitexture是否被支持,它可以使你同时把2个或多个不同的纹理映射到OpenGL图元上。开起来这个功能好像没有太大的作用,但当你使用多个纹理时,如果能同时把这些纹理值混合,而不使用费时的乘法运算,你将会得到很高的速度提高。

现在回到我们的代码,__ARB_ENABLE用来设置是否使用ARB扩展。如果你想看你的OpenGL扩展,只要把#define EXT_INFO前的注释去掉就行了。接着,我们在运行检查我们的扩展,以保证我们的程序可以在不同的系统上运行。所以我们需要一些内存保存扩展名的字符串,他们是下面两行。接着我们用一个变量multitextureSupported来标志当前系统是否能使用multitexture扩展,并用maxTexelUnits记录运行系统的纹理单元,这个值最少是1。
 
#define __ARB_ENABLE true       // 使用它设置是否使用ARB扩展
// #define EXT_INFO       // 把注释去掉,可以在启动时查看你的扩展
#define MAX_EXTENSION_SPACE 10240      // 保存扩展字符
#define MAX_EXTENSION_LENGTH 256      // 每个扩展字符串最大的长度
bool multitextureSupported=false;      // 标志是否支持多重渲染
bool useMultitexture=true;       // 如果支持,是否使用它
GLint maxTexelUnits=1;       // 纹理处理单元的个数

下面的函数定义用来使用OpenGL的扩展函数,你可以把PFN-who-ever-reads-this看成是预先定义的函数类型,因为我们不清楚是否能得到这些函数的实体,所以先把他们都设置为NULL。glMultiTexCoordifARB函数是glTexCoordif函数的扩展,它门的功能相似,其中i为纹理坐标的维数,f为数据的类型。最后两个函数用来激活纹理处理单元,可以使用特定的纹理单元来邦定纹理。
顺便说一句,ARB是"Architectural Review Board"的缩写,用来定义这个组织提出的对OpenGL的扩展,并不强制OpenGL的实现必须包含这个功能,但他们希望这个功能得到广泛的支持。当前,只有multitexture被加入到ARB中,这从另一个方面支持multitexture的扩展将大大的提高渲染速度。 
  
PFNGLMULTITEXCOORD1FARBPROC glMultiTexCoord1fARB = NULL;
PFNGLMULTITEXCOORD2FARBPROC glMultiTexCoord2fARB = NULL;
PFNGLMULTITEXCOORD3FARBPROC glMultiTexCoord3fARB = NULL;
PFNGLMULTITEXCOORD4FARBPROC glMultiTexCoord4fARB = NULL;
PFNGLACTIVETEXTUREARBPROC glActiveTextureARB = NULL;
PFNGLCLIENTACTIVETEXTUREARBPROC glClientActiveTextureARB= NULL;

下面我们来定义一些全局变量: 
filter定义过滤器类型
texture[3]保存三个纹理
bump[3]保存三个凹凸纹理
invbump[3]保存三个反转了的凹凸纹理
glLogo保存标志
multiLogo保存多重纹理标志
 
GLuint  filter=1;         // 定义过滤器类型
GLuint  texture[3];        // 保存三个纹理
GLuint  bump[3];         //保存三个凹凸纹理
GLuint  invbump[3];        // 保存三个反转了的凹凸纹理
GLuint  glLogo;         // glLogo保存标志
GLuint  multiLogo;         // multiLogo保存多重纹理标志
GLfloat LightAmbient[] = { 0.2f, 0.2f, 0.2f};     // 环境光
GLfloat LightDiffuse[] = { 1.0f, 1.0f, 1.0f};     // 漫射光
GLfloat LightPosition[] = { 0.0f, 0.0f, 2.0f};     // 光源位置
GLfloat Gray[]  = { 0.5f, 0.5f, 0.5f, 1.0f};

下面一块代码用来保存立方体的纹理和坐标,每5个数字描述一个顶点,包含2D的纹理坐标和3D的顶点坐标。 
  
// 立方体的纹理和坐标
GLfloat data[]= {
 // 前面
 0.0f, 0.0f,  -1.0f, -1.0f, +1.0f,
 1.0f, 0.0f,  +1.0f, -1.0f, +1.0f,
 1.0f, 1.0f,  +1.0f, +1.0f, +1.0f,
 0.0f, 1.0f,  -1.0f, +1.0f, +1.0f,
 // 背面
 1.0f, 0.0f,  -1.0f, -1.0f, -1.0f,
 1.0f, 1.0f,  -1.0f, +1.0f, -1.0f,
 0.0f, 1.0f,  +1.0f, +1.0f, -1.0f,
 0.0f, 0.0f,  +1.0f, -1.0f, -1.0f,
 // 上面
 0.0f, 1.0f,  -1.0f, +1.0f, -1.0f,
 0.0f, 0.0f,  -1.0f, +1.0f, +1.0f,
 1.0f, 0.0f,  +1.0f, +1.0f, +1.0f,
 1.0f, 1.0f,  +1.0f, +1.0f, -1.0f,
 // 下面
 1.0f, 1.0f,  -1.0f, -1.0f, -1.0f,
 0.0f, 1.0f,  +1.0f, -1.0f, -1.0f,
 0.0f, 0.0f,  +1.0f, -1.0f, +1.0f,
 1.0f, 0.0f,  -1.0f, -1.0f, +1.0f,
 // 右面
 1.0f, 0.0f,  +1.0f, -1.0f, -1.0f,
 1.0f, 1.0f,  +1.0f, +1.0f, -1.0f,
 0.0f, 1.0f,  +1.0f, +1.0f, +1.0f,
 0.0f, 0.0f,  +1.0f, -1.0f, +1.0f,
 // 左面
 0.0f, 0.0f,  -1.0f, -1.0f, -1.0f,
 1.0f, 0.0f,  -1.0f, -1.0f, +1.0f,
 1.0f, 1.0f,  -1.0f, +1.0f, +1.0f,
 0.0f, 1.0f,  -1.0f, +1.0f, -1.0f
};

下一部分代码,用来这运行时确定是否支持多重纹理的扩展。
首先,我们假定一个字符串包含了所有的扩展名,各个扩展名之间用'\n'分开。我们所要做的就是在其中查找是否有我们需要的扩展。如果成功找到则返回TRUE,否则返回FALSE。
 
bool isInString(char *string, const char *search) {
 int pos=0;
 int maxpos=strlen(search)-1;
 int len=strlen(string);
 char *other;
 for (int i=0; i<len; i++) {
  if ((i==0) || ((i>1) && string[i-1]=='\n')) {    // 新的扩展名开始与这里
   other=&string[i];
   pos=0;       // 开始新的比较
   while (string[i]!='\n') {     // 比较整个扩展名
    if (string[i]==search[pos]) pos++;   // 下一个字符
    if ((pos>maxpos) && string[i+1]=='\n') return true; // 如果整个扩展名相同则成功返回
    i++;
   }
  }
 }
 return false;        // 没找到
}

现在我们需要先取得扩展名字符串,并把它转换为以'\n'分割的字符串,接着调用以上定义的函数看看是否包含我们需要的扩展。如果定义了__ARB_ENABLE则使用多重纹理扩展,接下来我们检查是否支持GL_EXT_texture_env_combine扩展,这个扩展提供各个纹理单元复杂的交互方式,利用它可以完成复杂的混合方程。如果所有的扩展都被支持,我们首先取得纹理单元的个数,把它保存到变量maxTexelUnits中,接着通过函数wglGetProcAdress把各个函数定义连接到各自的实体上,这样在后面的程序中就可以使用这些函数了。 
  
bool initMultitexture(void) {
 char *extensions;
 extensions=strdup((char *) glGetString(GL_EXTENSIONS));   // 返回扩展名字符串
 int len=strlen(extensions);
 for (int i=0; i<len; i++)       // 使用'\n'分割各个扩展名
  if (extensions[i]==' ') extensions[i]='\n';

#ifdef EXT_INFO
 MessageBox(hWnd,extensions,"supported GL extensions",MB_OK | MB_ICONINFORMATION);
#endif

 if (isInString(extensions,"GL_ARB_multitexture")    // 是否支持多重纹理扩展?
  && __ARB_ENABLE       // 是否使用多重纹理扩展?
  && isInString(extensions,"GL_EXT_texture_env_combine"))  // 是否支持纹理环境混合
 {
  glGetIntegerv(GL_MAX_TEXTURE_UNITS_ARB,&maxTexelUnits);
  glMultiTexCoord1fARB = (PFNGLMULTITEXCOORD1FARBPROC) wglGetProcAddress("glMultiTexCoord1fARB");
  glMultiTexCoord2fARB = (PFNGLMULTITEXCOORD2FARBPROC) wglGetProcAddress("glMultiTexCoord2fARB");
  glMultiTexCoord3fARB = (PFNGLMULTITEXCOORD3FARBPROC) wglGetProcAddress("glMultiTexCoord3fARB");
  glMultiTexCoord4fARB = (PFNGLMULTITEXCOORD4FARBPROC) wglGetProcAddress("glMultiTexCoord4fARB");
  glActiveTextureARB   = (PFNGLACTIVETEXTUREARBPROC) wglGetProcAddress("glActiveTextureARB");
  glClientActiveTextureARB= (PFNGLCLIENTACTIVETEXTUREARBPROC) wglGetProcAddress("glClientActiveTextureARB");

#ifdef EXT_INFO
  MessageBox(hWnd,"The GL_ARB_multitexture 扩展被使用.","支持多重纹理",MB_OK | MB_ICONINFORMATION);
#endif

  return true;
 }
 useMultitexture=false;       // 如果不支持多重纹理则返回false
 return false;
}

初始化灯光 
  
void initLights(void) {
 glLightfv(GL_LIGHT1, GL_AMBIENT, LightAmbient);    
 glLightfv(GL_LIGHT1, GL_DIFFUSE, LightDiffuse);
 glLightfv(GL_LIGHT1, GL_POSITION, LightPosition);
 glEnable(GL_LIGHT1);
}

下面我们加载许多纹理,这和前面的教程很像 
  
int LoadGLTextures() {        // 载入*.bmp图像,并转换为纹理
 bool status=true;  
 AUX_RGBImageRec *Image=NULL;      
 char *alpha=NULL;

 // 加载基础纹理
 if (Image=auxDIBImageLoad("Data/Base.bmp")) {
  glGenTextures(3, texture);      // 创建3个纹理

  // 创建使用临近过滤器过滤得纹理
  glBindTexture(GL_TEXTURE_2D, texture[0]);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, Image->sizeX, Image->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Image->data);

  // 创建使用线形过滤器过滤得纹理
  glBindTexture(GL_TEXTURE_2D, texture[1]);
  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, GL_RGB8, Image->sizeX, Image->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE, Image->data);

  // 创建使用线形Mipmap过滤器过滤得纹理
  glBindTexture(GL_TEXTURE_2D, texture[2]);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR_MIPMAP_NEAREST);
  gluBuild2DMipmaps(GL_TEXTURE_2D, GL_RGB8, Image->sizeX, Image->sizeY, GL_RGB, GL_UNSIGNED_BYTE, Image->data);
 }
 else status=false;

 if (Image) {        // 如果图像句柄存在,则释放图像回收资源
  if (Image->data) delete Image->data;    
  delete Image;
  Image=NULL;
 }

现在我们加载凹凸映射纹理。这个纹理必须使用50%的亮度(原因我们在后面介绍),我们使用glPixelTransferf函数完成这个功能。
另一个限制是我们不希望纹理重复贴图,只希望它粘贴一次,从纹理坐标(0,0)-(1,1),所有大于它的纹理坐标都被映射到边缘,为了完成这个功能,我们使用glTexParameteri函数。 
  
 // 载入凹凸贴图
 if (Image=auxDIBImageLoad("Data/Bump.bmp")) {
  glPixelTransferf(GL_RED_SCALE,0.5f);     // 把颜色值变为原来的50%
  glPixelTransferf(GL_GREEN_SCALE,0.5f);    
  glPixelTransferf(GL_BLUE_SCALE,0.5f);
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_S,GL_CLAMP);  //不使用重复贴图
  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_WRAP_T,GL_CLAMP);
  glGenTextures(3, bump);      //创建凹凸贴图纹理

  // 创建使用临近过滤器过滤得纹理
  >…<
 // 创建使用线形过滤器过滤得纹理
  >…< // 创建使用线形Mipmap过滤器过滤得纹理
  >…<

反转凹凸贴图数据,创建三个反转的凹凸贴图纹理    

  for (int i=0; i<3*Image->sizeX*Image->sizeY; i++)   // 反转凹凸贴图数据
   Image->data[i]=255-Image->data[i];

  glGenTextures(3, invbump);      // 创建三个反转了凹凸贴图

  // 创建使用临近过滤器过滤得纹理
  >…< // 创建使用线形过滤器过滤得纹理
  >…< // 创建使用线形Mipmap过滤器过滤得纹理
  >…<
 }
 else status=false;
 if (Image) {        // 如果图像存在,则删除
  if (Image->data) delete Image->data; 
  delete Image;
  Image=NULL;
 }

载入标志图像,图像是把颜色和alpha通道存为两张不同的bmp位图的,所以在处理的时候需要注意以下各个分量的位置。 
  
 if (Image=auxDIBImageLoad("Data/OpenGL_ALPHA.bmp")) {
  alpha=new char[4*Image->sizeX*Image->sizeY];
  for (int a=0; a<Image->sizeX*Image->sizeY; a++)
   alpha[4*a+3]=Image->data[a*3];    
  if (!(Image=auxDIBImageLoad("Data/OpenGL.bmp"))) status=false;
  for (a=0; a<Image->sizeX*Image->sizeY; a++) {
   alpha[4*a]=Image->data[a*3];    
   alpha[4*a+1]=Image->data[a*3+1];  
   alpha[4*a+2]=Image->data[a*3+2];   
  }

  glGenTextures(1, &glLogo);      // 创建标志纹理

  // 使用线形过滤器
  glBindTexture(GL_TEXTURE_2D, glLogo);
  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, GL_RGBA8, Image->sizeX, Image->sizeY, 0, GL_RGBA, GL_UNSIGNED_BYTE, alpha);
  delete alpha;
 }
 else status=false;

 if (Image) {        // 如果图像存在,则删除
  if (Image->data) delete Image->data;    
  delete Image;
  Image=NULL;
 }

 // 载入扩展标志纹理
 if (Image=auxDIBImageLoad("Data/multi_on_alpha.bmp")) {
  alpha=new char[4*Image->sizeX*Image->sizeY];   
  >...<
  glGenTextures(1, &multiLogo);     
  >...<
  delete alpha;
 }
 else status=false;

 if (Image) {        // 如果图像存在,则删除
  if (Image->data) delete Image->data;    
  delete Image;
  Image=NULL;
 }
 return status;        
}

下面是窗口大小变化函数,没有任何改变。
接下来是绘制一个立方体的函数,它使用常规的方法绘制。
 
void doCube (void) {
 int i;
 glBegin(GL_QUADS);
  // 前面
  glNormal3f( 0.0f, 0.0f, +1.0f);
  for (i=0; i<4; i++) {
   glTexCoord2f(data[5*i],data[5*i+1]);
   glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
  }
  // 后面
  glNormal3f( 0.0f, 0.0f,-1.0f);
  for (i=4; i<8; i++) {
   glTexCoord2f(data[5*i],data[5*i+1]);
   glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
  }
  // 上面
  glNormal3f( 0.0f, 1.0f, 0.0f);
  for (i=8; i<12; i++) {
   glTexCoord2f(data[5*i],data[5*i+1]);
   glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
  }
  // 下面
  glNormal3f( 0.0f,-1.0f, 0.0f);
  for (i=12; i<16; i++) {
   glTexCoord2f(data[5*i],data[5*i+1]);
   glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
  }
  // 右面
  glNormal3f( 1.0f, 0.0f, 0.0f);
  for (i=16; i<20; i++) {
   glTexCoord2f(data[5*i],data[5*i+1]);
   glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
  }
  // 左面
  glNormal3f(-1.0f, 0.0f, 0.0f);
  for (i=20; i<24; i++) {
   glTexCoord2f(data[5*i],data[5*i+1]);
   glVertex3f(data[5*i+2],data[5*i+3],data[5*i+4]);
  }
 glEnd();
}

现在到了OpenGL的初始化函数,它和前面的教程基本相同,只是添加了以下代码: 
  
 multitextureSupported=initMultitexture();

 initLights(); 

这里我们完成了95%的工作,下面我们来解释上面提到的原理。
开始理论讲解(凹凸映射)
如果你安装了Powerpoint-viewer,下面是一个讲解凹凸映射原理的PPT,你可以下载后慢慢研究:
凹凸映射 作者Michael I. Gold, nVidia 公司
如果你没有安装Powerpoint-viewer,我把它转换为Html格式,现讲解如下:

凹凸贴图
Michael I. Gold
NVIDIA 公司

凹凸贴图

真实的凹凸贴图是逐像素计算的

光照计算是按每个象素点的法向量计算的
巨大的计算量
更多的信息可以看: Blinn, J. Simulation of Wrinkled Surfaces. Computer Graphics. 12, 3 (August 1978), 286-292
凹凸贴图是在效果和精度之间的一个折中

只能对漫射光计算,不能使用反射光
欺骗视觉的采样
可能运行于当前的硬件上
如果它看起来很好,就干吧
漫射光的计算

C = (L*N)*Dl*Dm

L 顶点到灯之间的单位向量
N 顶点的单位法向量
Dl 灯光的漫射光颜色
Dm 顶点材质的漫射属性
凸值 逐像素改变N值
凹凸映射 改变(L*N)的值
近似的漫射因子 L*N

纹理图代表高度场

[0,1] 之间的高度代表凹凸方程
首先导出偏离度m
m 增加/减少基础的漫射因子Fd
(Fd+m) 在每一像素上近似等于 (L*N)
偏移量m的导出

偏移量m的近似导出

查找(s,t)纹理的高度H0
查找(s+ds, t+dt)纹理的高度H1
M近似等于H1-H0
计算凹凸量


1) 原始凸起(H0).


2) 原始的凸起(H0)向光源移动一小段距离形成第二个凸起(H1)


3) 用H1凸起减去H0凸起 (H1-H0)

计算灯光亮度

计算片断的颜色Cf

Cf = (L*N) x Dl x Dm
(L*N) ~ (Fd + (H1-H0))
Ct= Dm x Dl
Cf = (Fd + (H0-H1) x Ct
Fd等于顶点法线与灯光的向量的乘积
上面就是全部么? 太简单了!

我们还没有完成所有的任务,还必须做以下内容:
创建一个纹理
计算纹理坐标偏移量ds, dt
计算漫射因子Fd
ds, dt ,Fd都从N和L导出
现在我们开始做一些数学计算
创建纹理

保存纹理!
当前的多重纹理硬件只支持两个纹理
偏移值保存在alpha通道里
最大凸起值为 = 1.0
水平面值为 = 0.5
最小值为= 0.0
颜色存储在RGB通道中
设置内部颜色格式为RGBA8 !!
计算纹理偏移量

把灯光方向向量变换到一个笛卡尔坐标系中
顶点法线为z轴
从法线和视口的“上”向量导出坐标系
顶点法线为z轴
叉乘得到X轴
丢弃“上”向量,利用z,y轴导出x轴
创建3x3变换矩阵Mn
变换灯光方向向量到这个坐标系中
计算纹理偏移量

使用法向坐标系中的向量作为偏移量
L'= Mn x L
使用L’.x, L’.y 作为 ds, dt
使用 L’.z 作为漫射因子!
如果灯光方向接近垂直,则L’.x, L’.y 非常小
如果灯光方向接近水平,则L’.x, L’.y 非常大
L’.z小于零的含义?
灯光在法线的对面
在TNT上的实现

计算向量,纹理坐标
设置漫射因子
从纹理单元0取出表面颜色和H0值
从纹理单元1取出H1值
ARB_multitexture 扩展
混合纹理扩展 (TBD)

混合0 alpha设置:
(1-T0a) + T1a - 0.5
T1a-T0a 映射到[-1,1],但硬件把它映射到[0,1]
T1a为H1的值,T0a为H0的值
0.5 平衡损失的掐除值
使用漫射光颜色调制(相乘)片断颜色T0c
混合1 颜色设置:
(T0c * C0a + T0c * Fda - 0.5 )*2
0.5 平衡损失的掐除值
乘以2加亮图像颜色
结束理论讲解(凹凸映射)

虽然我们做了一些改动,使得这个程序的实现与TNT的实现不一样,但它能工作与各种不同的显卡上。在这里我们将学到两三件事,凹凸映射在大多数显卡上是一个多通道算法(在TNT系列,可以使用一个2纹理通道实现),现在你应该能想到多重纹理的好处了吧。我们将使用一个三通道非多重纹理的算法实现,这个算法可以被改写为使用一个2纹理通道实现的算法。
现在必须告诉你,我们将要做一些矩阵和向量的乘法,但那没有什么可担心的,所有的矩阵和向量都使用齐次坐标。
 
// 计算向量v=v*M(左乘)
void VMatMult(GLfloat *M, GLfloat *v) {
 GLfloat res[3];
 res[0]=M[ 0]*v[0]+M[ 1]*v[1]+M[ 2]*v[2]+M[ 3]*v[3];
 res[1]=M[ 4]*v[0]+M[ 5]*v[1]+M[ 6]*v[2]+M[ 7]*v[3];
 res[2]=M[ 8]*v[0]+M[ 9]*v[1]+M[10]*v[2]+M[11]*v[3];
 v[0]=res[0];
 v[1]=res[1];
 v[2]=res[2];
 v[3]=M[15];        
}

开始理论讲解(凹凸映射)

开始,让我们看看它的算法

所有的向量必须在物体空间或则世界空间中
计算向量v,由灯的位置减去当前顶点的位置
归一化向量v
把向量v投影到切空间中
安向量v在切空间中的投影偏移纹理坐标
这看起来不错,它基本上和Michael I. Gold介绍的方法差不多。但它有一个缺点,它只对xy平面进行投影,这对我们的应用还是不够的。
但这个实现在计算漫射光的方法和我们是一样的,我们不能存储漫射因子,所以我们不能使用Michael I. Gold介绍的方法,因为我们想让它在任何显卡上运行而不仅仅是TNT系列。为什么不光照计算留到最后呢?这在简单的几何体绘制上是可行的,如果你需要渲染几千个具有凹凸贴图的三角形,你会感到绘制的速度不够快,到那时你需要改变这种渲染过程,寻找其它的方法。

在我们的实现里,它看起来和上面的实现差不多,除了投影部分,我们将使用我们自己的近似。

我们使用模型坐标,这种设定可以使得灯光位置相对于物体不变。
我们计算当前的顶点坐标
接着计算法线,并使它单位化
创建一个正投影矩阵,把灯光方向变为切空间
计算纹理坐标的偏移量,ds = s点乘v*MAX_EMBOSS, dt=t点乘v*MAX_EMBOSS
在通道2中,把偏移量添加到纹理坐标
为什么更好:
更快
看起来好看
这个方法可以工作与各种表面
可以运行于各种显卡
最大化的兼容
缺陷:
并不是完全的物理模拟
残留一些人为的假相 

 这个示意图显示了我们坐标系统,你可以通过相减相邻的坐标来获得s,t向量,但必须保证他们构成右手系和归一化。

结束理论讲解(凹凸映射)

下面让我们看看如何生成偏移量,首先创建一个函数创建凹凸映射:
 
// 设置纹理偏移,都为单位长度
// n : 表面的法向量
// c : 当前的顶点纹理坐标,返回纹理坐标的偏移量
// l : 灯光的位置
// s : s方向
// t : t方向
void SetUpBumps(GLfloat *n, GLfloat *c, GLfloat *l, GLfloat *s, GLfloat *t) {
 GLfloat v[3];        // 灯光方向
 GLfloat lenQ;        // 灯光方向向量的长度,使用它来单位化
 // 计算灯光方向
 v[0]=l[0]-c[0];
 v[1]=l[1]-c[1];
 v[2]=l[2]-c[2];
 lenQ=(GLfloat) sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);
 v[0]/=lenQ;
 v[1]/=lenQ;
 v[2]/=lenQ;
 // 把方向向量投影到s,t方向
 c[0]=(s[0]*v[0]+s[1]*v[1]+s[2]*v[2])*MAX_EMBOSS;
 c[1]=(t[0]*v[0]+t[1]*v[1]+t[2]*v[2])*MAX_EMBOSS;}
  
那看起来复杂么,但为了理解这个效果理论是必须的。(我在写这篇教程的时候也学习了它)。
我在程序运行的时候,总喜欢在屏幕上显示标志,现在我们有了两个,使用doLogo函数创建它。

下面的函数显示两个标志:一个OpenGL的标志,一个多重纹理的标志,如果可以使用多重纹理,则标志使用alpha混合,并看起来半透明。为了让它在屏幕的边沿显示我们使用混合并禁用光照和深度测试。

void doLogo(void) {
 // 必须最后在调用这个函数,以公告板的形式显示两个标志
 glDepthFunc(GL_ALWAYS);
 glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
 glEnable(GL_BLEND);
 glDisable(GL_LIGHTING);
 glLoadIdentity();
 glBindTexture(GL_TEXTURE_2D,glLogo);
 glBegin(GL_QUADS);
  glTexCoord2f(0.0f,0.0f); glVertex3f(0.23f, -0.4f,-1.0f);
  glTexCoord2f(1.0f,0.0f); glVertex3f(0.53f, -0.4f,-1.0f);
  glTexCoord2f(1.0f,1.0f); glVertex3f(0.53f, -0.25f,-1.0f);
  glTexCoord2f(0.0f,1.0f); glVertex3f(0.23f, -0.25f,-1.0f);
 glEnd();
 if (useMultitexture) {
  glBindTexture(GL_TEXTURE_2D,multiLogo);
  glBegin(GL_QUADS);
   glTexCoord2f(0.0f,0.0f); glVertex3f(-0.53f, -0.25f,-1.0f);
   glTexCoord2f(1.0f,0.0f); glVertex3f(-0.33f, -0.25f,-1.0f);
   glTexCoord2f(1.0f,1.0f); glVertex3f(-0.33f, -0.15f,-1.0f);
   glTexCoord2f(0.0f,1.0f); glVertex3f(-0.53f, -0.15f,-1.0f);
  glEnd();
 }
 glDepthFunc(GL_LEQUAL);
}

现在到了绘制凹凸贴图的函数了,我们先来看看不使用多重映射的方法,它通过三个通道实现。在第一步,我们先取得模型变换矩阵的逆矩阵! 
  
bool doMesh1TexelUnits(void) {
 GLfloat c[4]={0.0f,0.0f,0.0f,1.0f};     // 保存当前的顶点
 GLfloat n[4]={0.0f,0.0f,0.0f,1.0f};     // 保存法线
 GLfloat s[4]={0.0f,0.0f,0.0f,1.0f};     // s纹理坐标方向
 GLfloat t[4]={0.0f,0.0f,0.0f,1.0f};     // t纹理坐标方向
 GLfloat l[4];       // 保存灯光方向
 GLfloat Minv[16];       // 保存模型变换矩阵的逆
 int i;

 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // 清空背景颜色和深度缓存

 // 创建模型变换矩阵的逆
 glLoadIdentity();
 glRotatef(-yrot,0.0f,1.0f,0.0f);
 glRotatef(-xrot,1.0f,0.0f,0.0f);
 glTranslatef(0.0f,0.0f,-z);
 glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
 glLoadIdentity();
 glTranslatef(0.0f,0.0f,z);
 glRotatef(xrot,1.0f,0.0f,0.0f);
 glRotatef(yrot,0.0f,1.0f,0.0f);

 // 设置灯光的位置
 l[0]=LightPosition[0];
 l[1]=LightPosition[1];
 l[2]=LightPosition[2];
 l[3]=1.0f;        
 VMatMult(Minv,l);
 
 通道1:
使用凹凸纹理
禁止混合
禁止光照
使用无偏移的纹理坐标
绘制几何体
这将渲染一个无凹凸贴图的几何体 
  

 glBindTexture(GL_TEXTURE_2D, bump[filter]);
 glDisable(GL_BLEND);
 glDisable(GL_LIGHTING);
 doCube();

通道2:
使用反转的纹理凹凸贴图
设置混合因子为1,1
使用光照
使用偏移纹理坐标
绘制几何体
这将绘制一个具有凹凸贴图的几何体,但没有颜色 

 glBindTexture(GL_TEXTURE_2D,invbump[filter]);
 glBlendFunc(GL_ONE,GL_ONE);
 glDepthFunc(GL_LEQUAL);
 glEnable(GL_BLEND);

 glBegin(GL_QUADS);
  // 前面
  n[0]=0.0f;
  n[1]=0.0f;
  n[2]=1.0f;
  s[0]=1.0f;
  s[1]=0.0f;
  s[2]=0.0f;
  t[0]=0.0f;
  t[1]=1.0f;
  t[2]=0.0f;
  for (i=0; i<4; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   // 设置纹理坐标为偏移后的纹理坐标
   glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 后面
  n[0]=0.0f;
  n[1]=0.0f;
  n[2]=-1.0f;
  s[0]=-1.0f;
  s[1]=0.0f;
  s[2]=0.0f;
  t[0]=0.0f;
  t[1]=1.0f;
  t[2]=0.0f;
  for (i=4; i<8; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 上面
  n[0]=0.0f;
  n[1]=1.0f;
  n[2]=0.0f;
  s[0]=1.0f;
  s[1]=0.0f;
  s[2]=0.0f;
  t[0]=0.0f;
  t[1]=0.0f;
  t[2]=-1.0f;
  for (i=8; i<12; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 下面
  n[0]=0.0f;
  n[1]=-1.0f;
  n[2]=0.0f;
  s[0]=-1.0f;
  s[1]=0.0f;
  s[2]=0.0f;
  t[0]=0.0f;
  t[1]=0.0f;
  t[2]=-1.0f;
  for (i=12; i<16; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 右面
  n[0]=1.0f;
  n[1]=0.0f;
  n[2]=0.0f;
  s[0]=0.0f;
  s[1]=0.0f;
  s[2]=-1.0f;
  t[0]=0.0f;
  t[1]=1.0f;
  t[2]=0.0f;
  for (i=16; i<20; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 左面
  n[0]=-1.0f;
  n[1]=0.0f;
  n[2]=0.0f;
  s[0]=0.0f;
  s[1]=0.0f;
  s[2]=1.0f;
  t[0]=0.0f;
  t[1]=1.0f;
  t[2]=0.0f;
  for (i=20; i<24; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glTexCoord2f(data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
 glEnd();

通道3:
使用颜色纹理Use (colored) base-texture
使用混合因子GL_DST_COLOR, GL_SRC_COLOR
这个混合等于把颜色值乘以2
使用光照
绘制几何体
这个过程将结束立方体的渲染,因为我们可以在是否使用多重渲染之间切换,所以必须把纹理环境参数设为GL_MODULATE,这是默认的值。 
  

 if (!emboss) {
  glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
  glBindTexture(GL_TEXTURE_2D,texture[filter]);
  glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
  glEnable(GL_LIGHTING);
  doCube();
 }

最后的通道:
更新几何体
绘制标志

 xrot+=xspeed;
 yrot+=yspeed;
 if (xrot>360.0f) xrot-=360.0f;
 if (xrot<0.0f) xrot+=360.0f;
 if (yrot>360.0f) yrot-=360.0f;
 if (yrot<0.0f) yrot+=360.0f;

 //绘制标志
 doLogo();
 return true;       // 成功返回
}

这个函数将在多重纹理功能的支持下载两个通道中完成凹凸贴图的绘制,我们支持两个纹理单元,与一个纹理单元不同的是,我们给一个顶点设置两个纹理坐标。 
  
bool doMesh2TexelUnits(void) {
 GLfloat c[4]={0.0f,0.0f,0.0f,1.0f};     // 保存当前的顶点
 GLfloat n[4]={0.0f,0.0f,0.0f,1.0f};     // 保存法线
 GLfloat s[4]={0.0f,0.0f,0.0f,1.0f};     // s纹理坐标方向
 GLfloat t[4]={0.0f,0.0f,0.0f,1.0f};     // t纹理坐标方向
 GLfloat l[4];       // 保存灯光方向
 GLfloat Minv[16];       // 保存模型变换矩阵的逆
 int i;

 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // 清空背景颜色和深度缓存

 // 创建模型变换矩阵的逆
 glLoadIdentity();
 glRotatef(-yrot,0.0f,1.0f,0.0f);
 glRotatef(-xrot,1.0f,0.0f,0.0f);
 glTranslatef(0.0f,0.0f,-z);
 glGetFloatv(GL_MODELVIEW_MATRIX,Minv);
 glLoadIdentity();
 glTranslatef(0.0f,0.0f,z);

 glRotatef(xrot,1.0f,0.0f,0.0f);
 glRotatef(yrot,0.0f,1.0f,0.0f);

 // 设置灯光的位置
 l[0]=LightPosition[0];
 l[1]=LightPosition[1];
 l[2]=LightPosition[2];
 l[3]=1.0f;        
 VMatMult(Minv,l);

通道1:
无凹凸贴图
无光照
设置纹理混合器0
使用凹凸纹理
使用无偏移的纹理坐标
使用替换方式粘贴纹理
设置纹理混合器1
偏移纹理坐标
使用相加的纹理操作
这将绘制一个灰度的立方体 
  
 // 纹理单元 #0
 glActiveTextureARB(GL_TEXTURE0_ARB);
 glEnable(GL_TEXTURE_2D);
 glBindTexture(GL_TEXTURE_2D, bump[filter]);
 glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
 glTexEnvf (GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_REPLACE);

 // 纹理单元 #1
 glActiveTextureARB(GL_TEXTURE1_ARB);
 glEnable(GL_TEXTURE_2D);
 glBindTexture(GL_TEXTURE_2D, invbump[filter]);
 glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE_EXT);
 glTexEnvf (GL_TEXTURE_ENV, GL_COMBINE_RGB_EXT, GL_ADD);

 // 禁用混合和光照
 glDisable(GL_BLEND);
 glDisable(GL_LIGHTING);

现在按面一个一个的渲染立方体,和doMesh1TexelUnits函数中所作的操作差不多,只是用glMultiTexCoor2fARB替换glTexCoord2f,在这个函数中,你必须把纹理坐标发向不同的纹理处理单元,可用的参数值为GL_TEXTUREi_ARB0到GL_TEXTUREi_ARB31。 
  
 glBegin(GL_QUADS);
  // 前面
  n[0]=0.0f;
  n[1]=0.0f;
  n[2]=1.0f;
  s[0]=1.0f;
  s[1]=0.0f;
  s[2]=0.0f;
  t[0]=0.0f;
  t[1]=1.0f;
  t[2]=0.0f;
  for (i=0; i<4; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
   glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 后面
  n[0]=0.0f;
  n[1]=0.0f;
  n[2]=-1.0f;
  s[0]=-1.0f;
  s[1]=0.0f;
  s[2]=0.0f;
  t[0]=0.0f;
  t[1]=1.0f;
  t[2]=0.0f;
  for (i=4; i<8; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
   glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 上面
  n[0]=0.0f;
  n[1]=1.0f;
  n[2]=0.0f;
  s[0]=1.0f;
  s[1]=0.0f;
  s[2]=0.0f;
  t[0]=0.0f;
  t[1]=0.0f;
  t[2]=-1.0f;
  for (i=8; i<12; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
   glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 下面
  n[0]=0.0f;
  n[1]=-1.0f;
  n[2]=0.0f;
  s[0]=-1.0f;
  s[1]=0.0f;
  s[2]=0.0f;
  t[0]=0.0f;
  t[1]=0.0f;
  t[2]=-1.0f;
  for (i=12; i<16; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
   glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 右面
  n[0]=1.0f;
  n[1]=0.0f;
  n[2]=0.0f;
  s[0]=0.0f;
  s[1]=0.0f;
  s[2]=-1.0f;
  t[0]=0.0f;
  t[1]=1.0f;
  t[2]=0.0f;
  for (i=16; i<20; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
   glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
  // 左面
  n[0]=-1.0f;
  n[1]=0.0f;
  n[2]=0.0f;
  s[0]=0.0f;
  s[1]=0.0f;
  s[2]=1.0f;
  t[0]=0.0f;
  t[1]=1.0f;
  t[2]=0.0f;
  for (i=20; i<24; i++) {
   c[0]=data[5*i+2];
   c[1]=data[5*i+3];
   c[2]=data[5*i+4];
   SetUpBumps(n,c,l,s,t);
   glMultiTexCoord2fARB(GL_TEXTURE0_ARB,data[5*i], data[5*i+1]);
   glMultiTexCoord2fARB(GL_TEXTURE1_ARB,data[5*i]+c[0], data[5*i+1]+c[1]);
   glVertex3f(data[5*i+2], data[5*i+3], data[5*i+4]);
  }
 glEnd();

通道2:
使用基本纹理
使用光照
使用普通的纹理混合操作
这将完成最后的凹凸贴图 
  
 glActiveTextureARB(GL_TEXTURE1_ARB);
 glDisable(GL_TEXTURE_2D);
 glActiveTextureARB(GL_TEXTURE0_ARB);
 if (!emboss) {
  glTexEnvf (GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
  glBindTexture(GL_TEXTURE_2D,texture[filter]);
  glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
  glEnable(GL_BLEND);
  glEnable(GL_LIGHTING);
  doCube();
 }

最后的通道:
更新几何体
绘制标志

 xrot+=xspeed;
 yrot+=yspeed;
 if (xrot>360.0f) xrot-=360.0f;
 if (xrot<0.0f) xrot+=360.0f;
 if (yrot>360.0f) yrot-=360.0f;
 if (yrot<0.0f) yrot+=360.0f;

 doLogo();
 return true;        
}

最后绘制一个无凹凸贴图的立方体,用来观察两者之间的效果 
  
bool doMeshNoBumps(void) {
 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   
 glLoadIdentity();       
 glTranslatef(0.0f,0.0f,z);

 glRotatef(xrot,1.0f,0.0f,0.0f);
 glRotatef(yrot,0.0f,1.0f,0.0f);

 if (useMultitexture) {
  glActiveTextureARB(GL_TEXTURE1_ARB);
  glDisable(GL_TEXTURE_2D);
  glActiveTextureARB(GL_TEXTURE0_ARB);
 }

 glDisable(GL_BLEND);
 glBindTexture(GL_TEXTURE_2D,texture[filter]);
 glBlendFunc(GL_DST_COLOR,GL_SRC_COLOR);
 glEnable(GL_LIGHTING);
 doCube();

 xrot+=xspeed;
 yrot+=yspeed;
 if (xrot>360.0f) xrot-=360.0f;
 if (xrot<0.0f) xrot+=360.0f;
 if (yrot>360.0f) yrot-=360.0f;
 if (yrot<0.0f) yrot+=360.0f;

 doLogo();
 return true;        
}

所有的绘制函数都已经完成,接下来只要在绘制函数中调用即可 
  
bool DrawGLScene(GLvoid) 
{
 if (bumps) {
  if (useMultitexture && maxTexelUnits>1)
   return doMesh2TexelUnits();
  else return doMesh1TexelUnits();  }
 else return doMeshNoBumps();
}

删除OpenGL窗口 
  
GLvoid KillGLWindow(GLvoid)  
  
创建OpenGL窗口 
  
BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)

Windows循环 
  
LRESULT CALLBACK WndProc( HWND hWnd,
    UINT uMsg,     
    WPARAM wParam,     
    LPARAM lParam)     
在Windows主函数中加入一些控制键 :
E: 切换凹凸贴图模式中是否带有彩色纹理
M: 切换多重纹理模式
B: 切换是否使用凹凸贴图
F: 切换纹理过滤器模式
方向键: 旋转立方体  

    if (keys['E'])
    {
     keys['E']=false;
     emboss=!emboss;
    }

    if (keys['M'])
    {
     keys['M']=false;
     useMultitexture=((!useMultitexture) && multitextureSupported);
    }

    if (keys['B'])
    {
     keys['B']=false;
     bumps=!bumps;
    }

    if (keys['F'])
    {
     keys['F']=false;
     filter++;
     filter%=3;
    }

    if (keys[VK_PRIOR])
    {
     z-=0.02f;
    }

    if (keys[VK_NEXT])
    {
     z+=0.02f;
    }

    if (keys[VK_UP])
    {
     xspeed-=0.01f;
    }

    if (keys[VK_DOWN])
    {
     xspeed+=0.01f;
    }

    if (keys[VK_RIGHT])
    {
     yspeed+=0.01f;
    }

    if (keys[VK_LEFT])
    {
     yspeed-=0.01f;
    }

原文及其个版本源代码下载:

http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=22

 
 

 

posted @ 2016-12-31 16:25  wenglabs  阅读(1162)  评论(0编辑  收藏  举报