以文本方式查看主题

-  中文XML论坛 - 专业的XML技术讨论区  (http://bbs.xml.org.cn/index.asp)
--  『 C/C++编程思想 』  (http://bbs.xml.org.cn/list.asp?boardid=61)
----  [推荐]NeHe OpenGL教程(中英文版附带VC++源码)Lesson 33-lesson  34  (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=54271)


--  作者:一分之千
--  发布时间:10/24/2007 4:51:00 PM

--  [推荐]NeHe OpenGL教程(中英文版附带VC++源码)Lesson 33-lesson  34

第三十三 三十四课源码


第三十三课


按此在新窗口浏览图片加载压缩和未压缩的TGA文件:

在这一课里,你将学会如何加载压缩和为压缩的TGA文件,由于它使用RLE压缩,所以非常的简单,你能很快地熟悉它的。

  
   
   
我见过很多人在游戏开发论坛或其它地方询问关于TGA读取的问题。接下来的程序及注释将会向你展示如何读取未压缩的TGA文件和RLE压缩的文件。这个详细的教程适合于OpenGL,但是我计划改进它使其在将来更具普遍性。

我们将从两个头文件开始。第一个文件控制纹理结构,在第二个里,结构和变量将为程序读取所用。

就像每个头文件那样,我们需要一些包含保护措施以防止文件被重复包含。

在文件的顶部加入这样几行程序:

  
   

 #ifndef __TEXTURE_H__    // 看看此头文件是否已经被包含
 #define __TEXTURE_H__    // 如果没有,定义它

   
然后滚动到程序底部并添加:  
   

 #endif      // __TEXTURE_H__ 结束包含保护

   
这三行程序防止此文件被重复包含。文件中剩下的代码将处于这头两行和这最后一行之间。

在这个头文件中,我们将要加入完成每件工作所需的标准头文件。在#define __TGA_H__后添加如下几行:
  
   

 #pragma comment(lib, "OpenGL32.lib")  // 链接 Opengl32.lib
 #include <windows.h>   // 标准Windows头文件
 #include <stdio.h>    // 标准文件I/O头文件
 #include <gl\gl.h>    // 标准OpenGL头文件

   
第一个头文件是标准Windows头文件,第二个是为我们稍后的文件I/O所准备的,第三个是OpenGL32.lib所需的标准OpenGL头文件。

我们将需要一块空间存储图像数据以及OpenGL生成纹理所需的类型。我们将要用到以下结构:
  
   

 typedef struct
 {
  GLubyte* imageData;   // 控制整个图像的颜色值
  GLuint  bpp;    // 控制单位像素的bit数
  GLuint width;    // 整个图像的宽度
  GLuint height;    // 整个图像的高度
  GLuint texID;    // 使用glBindTexture所需的纹理ID.
  GLuint type;     // 描述存储在*ImageData中的数据(GL_RGB Or GL_RGBA)
 } Texture;

   
现在说说其它的,更长的头文件。同样我们需要一些包含保护措施,这和上述最后一个是一样的。

接下来,看看另外两个结构,它们将在处理TGA文件的过程中使用。

  
   

 typedef struct
 {
  GLubyte Header[12];   // 文件头决定文件类型
 } TGAHeader;

 typedef struct
 {
  GLubyte header[6];    // 控制前6个字节
  GLuint bytesPerPixel;   // 每像素的字节数 (3 或 4)
  GLuint imageSize;    // 控制存储图像所需的内存空间
  GLuint type;    // 图像类型 GL_RGB 或 GL_RGBA
  GLuint Height;    // 图像的高度
  GLuint Width;    // 图像宽度
  GLuint Bpp;    // 每像素的比特数 (24 或 32)
 } TGA;

   
现在我们声明那两个结构的一些实例,那样我们可以在程序中使用它们。  
   

 TGAHeader tgaheader;    // 用来存储我们的文件头
 TGA tga;      // 用来存储文件信息

   
我们需要定义一对文件头,那样我们能够告诉程序什么类型的文件头处于有效的图像上。如果是未压缩的TGA图像,前12字节将会是0 0 2 0 0 0 0 0 0 0 0 0,如果是RLE压缩的,则是0 0 10 0 0 0 0 0 0 0 0 0。这两个值允许我们检查正在读取的文件是否有效。
  
   

 // 未压缩的TGA头
 GLubyte uTGAcompare[12] = {0,0, 2,0,0,0,0,0,0,0,0,0};
 // 压缩的TGA头
 GLubyte cTGAcompare[12] = {0,0,10,0,0,0,0,0,0,0,0,0};

   
最后,我们声明两个函数用于读取过程。
  
   

 // 读取一个未压缩的文件
 bool LoadUncompressedTGA(Texture *, char *, FILE *);
 // 读取一个压缩的文件
 bool LoadCompressedTGA(Texture *, char *, FILE *);

   
现在,回到cpp文件,和程序中真正首当其冲部分,我将会省去一些错误消息处理代码并且使教程更短、更具可读性。你可以参看教程包含的文件(在文章的尾部有链接)。

马上,我们就可以在文件开头包含我们刚刚建立的头文件。
  
   

 #include "tga.h"    // 包含我们刚刚建立的头文件

   
不我们不需要包含其它任何文件了,因为我们已经在自己刚刚完成的头文件中包含他们了。

接下来,我们要做的事情是看看第一个函数,名为LoadTGA(…)。
  
   

 // 读取一个TGA文件!
 bool LoadTGA(Texture * texture, char * filename)
 {

   
它有两个参数。前者是一个指向纹理结构的指针,你必须在你的代码中声明它(见包含的例子)。后者是一个字符串,它告诉计算机在哪里去找你的纹理文件。

函数的前两行声明了一个文件指针,然后打开由“filename”参数指定的文件,它由函数的第二个指针传递进去。
  
   

 FILE * fTGA;     // 声明文件指针
 fTGA = fopen(filename, "rb");   // 以读模式打开文件

   
接下来的几行检查指定的文件是否已经正确地打开。  
   

 if(fTGA == NULL)    // 如果此处有错误
 {
  ...Error code...
  return false;    // 返回 False
 }

   
下一步,我们尝试读取文件的首12个字节的内容并且将它们存储在我们的TGAHeader结构中,这样,我们得以检查文件类型。如果fread失败,则关闭文件,显示一个错误,并且函数返回false。  
   

 if(fread(&tgaheader, sizeof(TGAHeader), 1, fTGA) == 0)
 {
  ...Error code here...
  return false;    //  如果失败则返回 False
 }

   
接着,通过我们用辛苦编的程序刚读取的头,我们继续尝试确定文件类型。这可以告诉我们它是压缩的、未压缩甚至是错误的文件类型。为了达到这个目的,我们将会使用memcmp(…)函数。  
   

 // 如果文件头附合未压缩的文件头格式
 if(memcmp(uTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)
 {
  // 读取未压缩的TGA文件
  LoadUncompressedTGA(texture, filename, fTGA);
 }
 // 如果文件头附合压缩的文件头格式
 else if(memcmp(cTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)
 {
  // 读取压缩的TGA格式
  LoadCompressedTGA(texture, filename, fTGA);
 }
 else      // 如果任一个都不符合
 {
  ...Error code here...
  return false;    // 返回 False
 }

   
我们将要开始读取一个未压缩格式文件的章节。

下面开始我们要做的第一件事,像往常一样,是函数头。
  
   

 //读取未压缩的TGA文件
 bool LoadUncompressedTGA(Texture * texture, char * filename, FILE * fTGA)
 {

   
这个函数有3个参数。头两个和LoadTGA中的一样,仅仅是简单的传递。第三个是来自前一个函数中的文件指针,因此我们没有丢失我们的空间。

接下来我们试着再从文件中读取6个字节的内容,并且存储在tga.header中。如果他失败了,我们运行一些错误处理代码,并且返回false。
  
   

 // 尝试继续读取6个字节的内容
 if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)
 {
  ...Error code here...
  return false;    // 返回 False
 }

   
现在我们有了计算图像的高度、宽度和BPP的全部信息。我们在纹理和本地结构中都将存储它。  
   

 texture->width  = tga.header[1] * 256 + tga.header[0]; // 计算高度
 texture->height = tga.header[3] * 256 + tga.header[2]; // 计算宽度
 texture->bpp = tga.header[4];   // 计算BPP
 tga.Width = texture->width;    // 拷贝Width到本地结构中去
 tga.Height = texture->height;   // 拷贝Height到本地结构中去
 tga.Bpp = texture->bpp;    // 拷贝Bpp到本地结构中去

   
现在,我们需要确认高度和宽度至少为1个像素,并且bpp是24或32。如果这些值中的任何一个超出了它们的界限,我们将再一次显示一个错误,关闭文件,并且离开此函数。  
   

 // 确认所有的信息都是有效的
 if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp !=32)))
 {
  ...Error code here...
  return false;    // 返回 False
 }

   
接下来我们设置图像的类型。24 bit图像是GL_RGB,32 bit 图像是GL_RGBA  
   

 if(texture->bpp == 24)    // 是24 bit图像吗?
 {
  texture->type = GL_RGB;   //如果是,设置类型为GL_RGB
 }
 else      // 如果不是24bit,则必是32bit
 {
  texture->type = GL_RGBA;  //这样设置类型为GL_RGBA
 }

   
现在我们计算每像素的字节数和总共的图像数据。  
   

 tga.bytesPerPixel = (tga.Bpp / 8);  // 计算BPP
 // 计算存储图像所需的内存
 tga.imageSize = (tga.bytesPerPixel * tga.Width * tga.Height);

   
我们需要一些空间去存储整个图像数据,因此我们将要使用malloc分配正确的内存数量

然后我们确认内存已经分配,并且它不是NULL。如果出现了错误,则运行错误处理代码。
  
   

 // 分配内存
 texture->imageData = (GLubyte *)malloc(tga.imageSize);
 if(texture->imageData == NULL)   // 确认已经分配成功
 {
  ...Error code here...
  return false;    // 确认已经分配成功
 }

   
这里我们尝试读取所有的图像数据。如果不能,我们将再次触发错误处理代码。  
   

 // 尝试读取所有图像数据
 if(fread(texture->imageData, 1, tga.imageSize, fTGA) != tga.imageSize)
 {
  ...Error code here...
  return false;    // 如果不能,返回false
 }

   
TGA文件用逆OpenGL需求顺序的方式存储图像,因此我们必须将格式从BGR到RGB。为了达到这一点,我们交换每个像素的第一个和第三个字节的内容。

Steve Thomas补充:我已经编写了能稍微更快速读取TGA文件的代码。它涉及到仅用3个二进制操作将BGR转换到RGB的方法。

然后我们关闭文件,并且成功退出函数。
  
   

 //  开始循环
 for(GLuint cswap = 0; cswap < (int)tga.imageSize; cswap += tga.bytesPerPixel)
 {
  // 第一字节 XOR第三字节XOR 第一字节 XOR 第三字节
  texture->imageData[cswap] ^= texture->imageData[cswap+2] ^=
  texture->imageData[cswap] ^= texture->imageData[cswap+2];
 }

 fclose(fTGA);     // 关闭文件
 return true;     // 返回成功
}

   
以上是读取未压缩型TGA文件的方法。读取RLE压缩型文件的步骤稍微难一点。我们像平时一样读取文件头并且收集高度/宽度/色彩深度,这和读取未压缩版本是一致的。  
   

 bool LoadCompressedTGA(Texture * texture, char * filename, FILE * fTGA)
 {
  if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)
  {
   ...Error code here...
  }
  texture->width  = tga.header[1] * 256 + tga.header[0];
  texture->height = tga.header[3] * 256 + tga.header[2];
  texture->bpp = tga.header[4];
  tga.Width = texture->width;
  tga.Height = texture->height;
  tga.Bpp = texture->bpp;
  if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp !=32)))
  {
   ...Error code here...
  }        }
  tga.bytesPerPixel = (tga.Bpp / 8);
  tga.imageSize  = (tga.bytesPerPixel * tga.Width * tga.Height);

   
现在我们需要分配存储图像所需的空间,这是为我们解压缩之后准备的,我们将使用malloc。如果内存分配失败,运行错误处理代码,并且返回false。  
   

 // 分配存储图像所需的内存空间
 texture->imageData = (GLubyte *)malloc(tga.imageSize);
 if(texture->imageData == NULL)   // 如果不能分配内存
 {
  ...Error code here...
  return false;    // 返回 False
 }

   
下一步我们需要决定组成图像的像素数。我们将它存储在变量“pixelcount”中。

我们也需要存储当前所处的像素,以及我们正在写入的图像数据的字节,这样避免溢出写入过多的旧数据。


我们将要分配足够的内存来存储一个像素。
  
   

 GLuint pixelcount = tga.Height * tga.Width; // 图像中的像素数
 GLuint currentpixel = 0;  // 当前正在读取的像素
 GLuint currentbyte = 0;   // 当前正在向图像中写入的像素
// 一个像素的存储空间
 GLubyte * colorbuffer = (GLubyte *)malloc(tga.bytesPerPixel);

   
接下来我们将要进行一个大循环。

让我们将它分解为更多可管理的块。

首先我们声明一个变量来存储“块”头。块头指示接下来的段是RLE还是RAW,它的长度是多少。如果一字节头小于等于127,则它是一个RAW头。头的值是颜色数,是负数,在我们处理其它头字节之前,我们先读取它并且拷贝到内存中。这样我们将我们得到的值加1,然后读取大量像素并且将它们拷贝到ImageData中,就像我们处理未压缩型图像一样。如果头大于127,那么它是下一个像素值随后将要重复的次数。要获取实际重复的数量,我们将它减去127以除去1bit的的头标示符。然后我们读取下一个像素并且依照上述次数连续拷贝它到内存中。
  
   

 do      // 开始循环
 {
 GLubyte chunkheader = 0;    // 存储Id块值的变量
 if(fread(&chunkheader, sizeof(GLubyte), 1, fTGA) == 0) // 尝试读取块的头
 {
  ...Error code...
  return false;    // If It Fails, Return False
 }

   
接下来我们将要看看它是否是RAW头。如果是,我们需要将此变量的值加1以获取紧随头之后的像素总数。  
   

 if(chunkheader < 128)    // 如果是RAW块
 {
  chunkheader++;    // 变量值加1以获取RAW像素的总数

   
我们开启另一个循环读取所有的颜色信息。它将会循环块头中指定的次数,并且每次循环读取和存储一个像素。

首先,我们读取并检验像素数据。单个像素的数据将被存储在colorbuffer变量中。然后我们将检查它是否为RAW头。如果是,我们需要添加一个到变量之中以获取头之后的像素总数。
  
   

 // 开始像素读取循环
 for(short counter = 0; counter < chunkheader; counter++)
 {
  // 尝试读取一个像素
  if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)
  {
   ...Error code...
   return false;   // 如果失败,返回false
  }

   
我们循环中的下一步将要获取存储在colorbuffer中的颜色值并且将其写入稍后将要使用的imageData变量中。在这个过程中,数据格式将会由BGR翻转为RGB或由BGRA转换为RGBA,具体情况取决于每像素的比特数。当我们完成任务后我们增加当前的字节和当前的像素计数器。  
   

 texture->imageData[currentbyte] = colorbuffer[2];  // 写“R”字节
 texture->imageData[currentbyte + 1 ] = colorbuffer[1]; //写“G”字节
 texture->imageData[currentbyte + 2 ] = colorbuffer[0]; // 写“B”字节
 if(tga.bytesPerPixel == 4)     // 如果是32位图像...
 {
  texture->imageData[currentbyte + 3] = colorbuffer[3]; // 写“A”字节
 }
 // 依据每像素的字节数增加字节计数器
 currentbyte += tga.bytesPerPixel;
 currentpixel++;     // 像素计数器加1
   
下一段处理描述RLE段的“块”头。首先我们将chunkheader减去127来得到获取下一个颜色重复的次数。  
   

 else      // 如果是RLE头
 {
  chunkheader -= 127;   //  减去127获得ID Bit的Rid

   
然后我们尝试读取下一个颜色值。  
   

 // 读取下一个像素
 if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)
 {
  ...Error code...
  return false;    // 如果失败,返回false
 }

   
接下来,我们开始循环拷贝我们多次读到内存中的像素,这由RLE头中的值规定。

然后,我们将颜色值拷贝到图像数据中,预处理R和B的值交换。

随后,我们增加当前的字节数、当前像素,这样我们再次写入值时可以处在正确的位置。

  
   

 // 开始循环
 for(short counter = 0; counter < chunkheader; counter++)
 {
  // 拷贝“R”字节
  texture->imageData[currentbyte] = colorbuffer[2];
  // 拷贝“G”字节
  texture->imageData[currentbyte + 1 ] = colorbuffer[1];
  // 拷贝“B”字节
  texture->imageData[currentbyte + 2 ] = colorbuffer[0];
  if(tga.bytesPerPixel == 4)  // 如果是32位图像
  {
   // 拷贝“A”字节
   texture->imageData[currentbyte + 3] = colorbuffer[3];
  }
  currentbyte += tga.bytesPerPixel; // 增加字节计数器
  currentpixel++;   // 增加字节计数器
   
只要仍剩有像素要读取,我们将会继续主循环。

最后,我们关闭文件并返回成功。

  
   

  while(currentpixel < pixelcount); // 是否有更多的像素要读取?开始循环直到最后
  fclose(fTGA);   // 关闭文件
  return true;   // 返回成功
 }

   
现在你已经为glGenTextures和glBindTexture准备好了数据。我建议你查看Nehe的教程6和24以获取这些命令的更多信息。那证实了我先前写的教程的正确性,我不确保的代码中没有错误,虽然我努力使之不发生错误。特别感谢Jeff“Nehe”Molofee写了这个伟大的教程,以及Trent“ShiningKnight”Polack帮助我修订这个教程。如果你发现了错误、有建议或者注释,请自由地给我发Email



--  作者:一分之千
--  发布时间:10/24/2007 4:52:00 PM

--  
Lesson 33
   
Loading Uncompressed and RunLength Encoded TGA images By Evan "terminate" Pipho.

I've seen lots of people ask around in #gamdev, the gamedev forums, and other places about TGA loading. The following code and explanation will show you how to load both uncompressed TGA files and RLE compressed files. This particular tutorial is geared toward OpenGL, but I plan to make it more universal in the future.

We will begin with the two header files. The first file will hold our texture structure, the second, structures and variables used by the loading code.

Like every header file we need some inclusion guards to prevent the file from being included multiple times.

At the top of the file add these lines:   
   

 #ifndef __TEXTURE_H__    // See If The Header Has Been Defined Yet
 #define __TEXTURE_H__    // If Not, Define It.

   
Then scroll all the way down to the bottom and add:   
   

 #endif      // __TEXTURE_H__ End Inclusion Guard

   
These three lines prevent the file from being included more than once into a file. The rest of the code in the file will be between the first two, and the last line.

Into this header file we we will insert the standard headers we will need for everything we do. Add the following lines after the #define __TGA_H__ command.   
   

 #pragma comment(lib, "OpenGL32.lib")  // Link To Opengl32.lib
 #include <windows.h>    // Standard Windows header
 #include <stdio.h>    // Standard Header For File I/O
 #include <gl\gl.h>    // Standard Header For OpenGL

   
The first header is the standard windows header, the second is for the file I/O functions we will be using later, and the 3rd is the standard OpenGL header file for OpenGL32.lib.

We will need a place to store image data and specifications for generating a texture usable by OpenGL. We will use the following structure.   
   

 typedef struct
 {
  GLubyte* imageData;   // Hold All The Color Values For The Image.
  GLuint  bpp;    // Hold The Number Of Bits Per Pixel.   
  GLuint width;    // The Width Of The Entire Image. 
  GLuint height;    // The Height Of The Entire Image. 
  GLuint texID;    // Texture ID For Use With glBindTexture. 
  GLuint type;     // Data Stored In * ImageData (GL_RGB Or GL_RGBA)
 } Texture;

   
Now for the other, longer head file. Again we will need some includsion guards, same as the last one.

Next come two more structures used during processing of the TGA file.   
   

 typedef struct
 {
  GLubyte Header[12];   // File Header To Determine File Type
 } TGAHeader;

 typedef struct
 {
  GLubyte header[6];   // Holds The First 6 Useful Bytes Of The File
  GLuint bytesPerPixel;   // Number Of BYTES Per Pixel (3 Or 4)
  GLuint imageSize;   // Amount Of Memory Needed To Hold The Image
  GLuint type;    // The Type Of Image, GL_RGB Or GL_RGBA
  GLuint Height;    // Height Of Image     
  GLuint Width;    // Width Of Image    
  GLuint Bpp;    // Number Of BITS Per Pixel (24 Or 32)
 } TGA;

   
Now we declare some instances of our two structures so we can use them within our code.   
   

 TGAHeader tgaheader;    // Used To Store Our File Header
 TGA tga;     // Used To Store File Information

   
We need to define a couple file headers so we can tell the program what kinds of file headers are on valid images. The first 12 bytes will be 0 0 2 0 0 0 0 0 0 0 0 0 if it is uncompressed TGA image and 0 0 10 0 0 0 0 0 0 0 0 0 if it an RLE compressed one. These two values allow us to check to see if the file we are reading is valid.   
   

 // Uncompressed TGA Header
 GLubyte uTGAcompare[12] = {0,0, 2,0,0,0,0,0,0,0,0,0};
 // Compressed TGA Header
 GLubyte cTGAcompare[12] = {0,0,10,0,0,0,0,0,0,0,0,0};

   
Finally we declare a pair of functions to use in the loading process.   
   

 // Load An Uncompressed File
 bool LoadUncompressedTGA(Texture *, char *, FILE *);
 // Load A Compressed File
 bool LoadCompressedTGA(Texture *, char *, FILE *);

   
Now, on to the cpp file, and the real brunt of the code. I will leave out some of the error message code and the like to make the tutorial shorter and more readable. You may look in the included file (link at the bottom of the article)

Right off the bat we have to include the file we just made so at the top of the file put.   
   

 #include "tga.h"    // Include The Header We Just Made

   
We don't have to include any other files, because we included them inside our header we just completed.

The next thing we see is the first function, which is called LoadTGA(...).   
   

 // Load A TGA File!
 bool LoadTGA(Texture * texture, char * filename)
 {

   
It takes two parameters. The first is a pointer to a Texture structure, which you must have declared in your code some where (see included example). The second parameter is a string that tells the computer where to find your texture file.

The first two lines of the function declare a file pointer and then open the file specified by "filename" which was passed to the function in the second parem for reading.   
   

 FILE * fTGA;     // Declare File Pointer
 fTGA = fopen(filename, "rb");   // Open File For Reading

   
The next few lines check to make sure that the file opened correctly.   
   

 if(fTGA == NULL)    // If Here Was An Error
 {
  ...Error code...
  return false;    // Return False
 }

   
Next we try to read the first twelve bytes of the file and store them in out TGAHeader structure so we can check on the file type. If fread fails, the file is closed, an error displayed, and the function returns false.   
   

 // Attempt To Read The File Header
 if(fread(&tgaheader, sizeof(TGAHeader), 1, fTGA) == 0)
 {
  ...Error code here...
  return false;    // Return False If It Fails
 }

   
Next we try to determine what type of file it is by comparing our newly aquired header with our two hard coded ones. This will tell us if its compressed, uncompressed, or even if its the wrong file type. For this purpose we will use the memcmp(...) function.   
   

 // If The File Header Matches The Uncompressed Header
 if(memcmp(uTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)
 {
  // Load An Uncompressed TGA
  LoadUncompressedTGA(texture, filename, fTGA);
 }
 // If The File Header Matches The Compressed Header
 else if(memcmp(cTGAcompare, &tgaheader, sizeof(tgaheader)) == 0)
 {
  // Load A Compressed TGA
  LoadCompressedTGA(texture, filename, fTGA);
 }
 else      // If It Doesn't Match Either One
 {
  ...Error code here...
  return false;    // Return False
 }

   
We will begin this section with the loading of an UNCOMPRESSED file. This function is heavily based on NeHe's code in lesson 25.

First thing we come to, as always, is the function header.   
   

 // Load An Uncompressed TGA!
 bool LoadUncompressedTGA(Texture * texture, char * filename, FILE * fTGA)
 {

   
This function takes three parameters. The first two are the same as from LoadTGA, and are simply passed on. The third is the file pointer from the previous function so that we dont lose our place.

Next we try to read the next 6 bytes of the file, and store them in tga.header. If it fails, we run some error code, and return false.   
   

 // Attempt To Read Next 6 Bytes
 if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)
 {
  ...Error code here...
  return false;    // Return False
 }

   
Now we have all the information we need to calculate the height, width and bpp of our image. We store it in both the texture and local structure.   
   

 texture->width  = tga.header[1] * 256 + tga.header[0]; // Calculate Height
 texture->height = tga.header[3] * 256 + tga.header[2]; // Calculate The Width
 texture->bpp = tga.header[4];    // Calculate Bits Per Pixel
 tga.Width = texture->width;    // Copy Width Into Local Structure
 tga.Height = texture->height;    // Copy Height Into Local Structure
 tga.Bpp = texture->bpp;     // Copy Bpp Into Local Structure

   
Now we check to make sure that the height and width are at least one pixel, and that the bpp is either 24 or 32. If any of the values are outside their boundries we again display an error, close the file, and leave the function.   
   

 // Make Sure All Information Is Valid
 if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp !=32)))
 {
  ...Error code here...
  return false;    // Return False
 }

   
Next we set the type of the image. 24 bit images are type GL_RGB and 32 bit images are type GL_RGBA   
   

 if(texture->bpp == 24)    // Is It A 24bpp Image?
 {
  texture->type = GL_RGB;  // If So, Set Type To GL_RGB
 }
 else      // If It's Not 24, It Must Be 32
 {
  texture->type = GL_RGBA;  // So Set The Type To GL_RGBA
 }

   
Now we caclulate the BYTES per pixel and the total size of the image data.   
   

 tga.bytesPerPixel = (tga.Bpp / 8);  // Calculate The BYTES Per Pixel
 // Calculate Memory Needed To Store Image
 tga.imageSize = (tga.bytesPerPixel * tga.Width * tga.Height);

   
We need some place to store all that image data so we will use malloc to allocate the right amount of memory.

Then we check to make sure memory was allocated, and is not NULL. If there was an error, run error handling code.   
   

 // Allocate Memory
 texture->imageData = (GLubyte *)malloc(tga.imageSize);
 if(texture->imageData == NULL)   // Make Sure It Was Allocated Ok
 {
  ...Error code here...
  return false;    // If Not, Return False
 }

   
Here we try to read all the image data. If we can't, we trigger error code again.   
   

 // Attempt To Read All The Image Data
 if(fread(texture->imageData, 1, tga.imageSize, fTGA) != tga.imageSize)
 {
  ...Error code here...
  return false;    // If We Cant, Return False
 }

   
TGA files store their image in reverse order than what OpenGL wants so we much change the format from BGR to RGB. To do this we swap the first and third bytes in every pixel.

Steve Thomas Adds: I've got a little speedup in TGA loading code. It concerns switching BGR into RGB using only 3 binary operations. Instead of using a temp variable you XOR the two bytes 3 times.

Then we close the file, and exit the function successfully.   
   

 // Start The Loop
 for(GLuint cswap = 0; cswap < (int)tga.imageSize; cswap += tga.bytesPerPixel)
 {
  // 1st Byte XOR 3rd Byte XOR 1st Byte XOR 3rd Byte
  texture->imageData[cswap] ^= texture->imageData[cswap+2] ^=
  texture->imageData[cswap] ^= texture->imageData[cswap+2];
 }

 fclose(fTGA);     // Close The File
 return true;     // Return Success
}

   
Thats all there is to loading an uncompressed TGA file. Loading a RLE compressed one is only slightly harder. We read the header and collect height/width/bpp as usual, sme as the uncompressed version, so i will just post the code, you can look in the previous pages for a complete explanation.   
   

 bool LoadCompressedTGA(Texture * texture, char * filename, FILE * fTGA)
 {
  if(fread(tga.header, sizeof(tga.header), 1, fTGA) == 0)
  {
   ...Error code here...
  }
  texture->width  = tga.header[1] * 256 + tga.header[0];
  texture->height = tga.header[3] * 256 + tga.header[2];
  texture->bpp = tga.header[4];
  tga.Width = texture->width;
  tga.Height = texture->height;
  tga.Bpp = texture->bpp;
  if((texture->width <= 0) || (texture->height <= 0) || ((texture->bpp != 24) && (texture->bpp !=32)))
  {
   ...Error code here...
  }        }
  tga.bytesPerPixel = (tga.Bpp / 8);
  tga.imageSize  = (tga.bytesPerPixel * tga.Width * tga.Height);

   
Now we need to allocate the amount of storage for the image to use AFTER we uncompress it, we will use malloc. If memory fails to be allocated, run error code, and return false.   
   

 // Allocate Memory To Store Image Data
 texture->imageData = (GLubyte *)malloc(tga.imageSize);
 if(texture->imageData == NULL)   // If Memory Can Not Be Allocated...
 {
  ...Error code here...
  return false;    // Return False
 }

   
Next we need to determine how many pixels make up the image. We will store it in the variable "pixelcount"

We also need to store which pixel we are currently on, and what byte of the imageData we are writing to, to avoid overflows and overwriting old data.

We will allocate enough memory to store one pixel.   
   

 GLuint pixelcount = tga.Height * tga.Width; // Number Of Pixels In The Image
 GLuint currentpixel = 0;   // Current Pixel We Are Reading From Data
 GLuint currentbyte = 0;   // Current Byte We Are Writing Into Imagedata
 // Storage For 1 Pixel
 GLubyte * colorbuffer = (GLubyte *)malloc(tga.bytesPerPixel);

   
Next we have a big loop.

Lets break it down into more manageable chunks.

First we declare a variable in order to store the chunk header. A chunk header dictates wheather the following section is RLE, or RAW, and how long it is. If the one byte header is less than or equal to 127, then it is a RAW header. The value of the header is the number of colors, minus one, that we read ahead and copy into memory, before we hit another header byte. So we add one to the value we get, and then read that many pixels and copy them into the ImageData, just like we did with the uncompressed ones. If the header is ABOVE 127, then it is the number of times that the next pixel value is repeated consequtively. To get the actual number of repetitions we take the value returned and subtract 127 to get rid of the one bit header identifier. Then we read the next one pixel and copy it the said number of times consecutively into the memory.

On to the code. First we read the one byte header.   
   

 do      // Start Loop
 {
 GLubyte chunkheader = 0;   // Variable To Store The Value Of The Id Chunk
 if(fread(&chunkheader, sizeof(GLubyte), 1, fTGA) == 0) // Attempt To Read The Chunk's Header
 {
  ...Error code...
  return false;    // If It Fails, Return False
 }

   
Next we will check to see if it a RAW header. If it is, we need to add one to the value to get the total number of pixels following the header.   
   

 if(chunkheader < 128)    // If The Chunk Is A 'RAW' Chunk
 {
  chunkheader++;    // Add 1 To The Value To Get Total Number Of Raw Pixels

   
We then start another loop to read all the color information. It will loop the amout of times specified in the chunk header, and will read and store one pixel each loop.

First we read and verify the pixel data. The data for one pixel will be stored in the colorbuffer variable. Next we will check to see if it a RAW header. If it is, we need to add one to the value to get the total number of pixels following the header.   
   

 // Start Pixel Reading Loop
 for(short counter = 0; counter < chunkheader; counter++)
 {
  // Try To Read 1 Pixel
  if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)
  {
   ...Error code...
   return false;   // If It Fails, Return False
  }

   
The next part in our loop will take the color values stored in colorbuffer and writing them to the imageData varable to be used later. In the process it will flip the data from BGR format to RGB or from BGRA to RGBA depending on the number of bits per pixel. When we are done we increment the current byte, and current pixel counters.   
   

 texture->imageData[currentbyte] = colorbuffer[2];  // Write The 'R' Byte
 texture->imageData[currentbyte + 1 ] = colorbuffer[1]; // Write The 'G' Byte
 texture->imageData[currentbyte + 2 ] = colorbuffer[0]; // Write The 'B' Byte
 if(tga.bytesPerPixel == 4)     // If It's A 32bpp Image...
 {
  texture->imageData[currentbyte + 3] = colorbuffer[3]; // Write The 'A' Byte
 }
 // Increment The Byte Counter By The Number Of Bytes In A Pixel
 currentbyte += tga.bytesPerPixel;
 currentpixel++;     // Increment The Number Of Pixels By 1

   
The next section deals with the chunk headers that represent the RLE sections. First thing we do is subtract 127 from the chunkheader to get the amount of times the next color is repeated.   
   

 else      // If It's An RLE Header
 {
  chunkheader -= 127;   // Subtract 127 To Get Rid Of The ID Bit

   
The we attempt to read the next color value.   
   

 // Read The Next Pixel
 if(fread(colorbuffer, 1, tga.bytesPerPixel, fTGA) != tga.bytesPerPixel)
 {
  ...Error code...
  return false;    // If It Fails, Return False
 }

   
Next we begin a loop to copy the pixel we just read into memory multiple times, as dictated by the value from the RLE header.

Then we copy the the color values into the image data, preforming the R and B value switch.

Then we increment the current bytes, and current pixel, so we are in the right spot when we write the values again.   
   

 // Start The Loop
 for(short counter = 0; counter < chunkheader; counter++)
 {
  // Copy The 'R' Byte
  texture->imageData[currentbyte] = colorbuffer[2];
  // Copy The 'G' Byte
  texture->imageData[currentbyte + 1 ] = colorbuffer[1];
  // Copy The 'B' Byte
  texture->imageData[currentbyte + 2 ] = colorbuffer[0];
  if(tga.bytesPerPixel == 4)  // If It's A 32bpp Image
  {
   // Copy The 'A' Byte
   texture->imageData[currentbyte + 3] = colorbuffer[3];
  }
  currentbyte += tga.bytesPerPixel; // Increment The Byte Counter
  currentpixel++;    // Increment The Pixel Counter

   
Then we contiune the main loop, as long as we still have pixels left to read.

Last of all we close up the file and return success.   
   

  while(currentpixel < pixelcount); // More Pixels To Read? ... Start Loop Over
  fclose(fTGA);    // Close File
  return true;    // Return Success
 }

   
Now you have some image data ready for glGenTextures and glBindTexture. I suggest you check out NeHe's tutorial #6 and #24 for info on these commands. That concludes my first ever tutorial. I do not guarantee my code is error free, though i made an effort to see that it was. Special thanks to Jeff "NeHe" Molofee for his great tutorials and to Trent "ShiningKnight" Polack for helping me revise this tutorial. If you find errors, have suggestions, or comments please feel free to email me (terminate@gdnmail.net), or ICQ me at UIN# 38601160. Enjoy!

Evan Pipho (Terminate)

Jeff Molofee (NeHe)


--  作者:一分之千
--  发布时间:10/24/2007 4:53:00 PM

--  

第三十四课


按此在新窗口浏览图片从高度图生成地形:

这一课将教会你如何从一个2D的灰度图创建地形

  
   
   
欢迎来到新的一课,Ben Humphrey写了这一课的代码,它是基于第一课所写的。
在这一课里,我们将教会你如何使用地形,你将知道高度图这个概念。
  
   
   
下面我们来定义一些全局变量,MAP_SIZE是你使用的高度图的大小,在这一课里我们使用1024*1024的地图。STEP_SIZE设置高度图中相邻顶点之间的距离。HEIGHT_RATIO设置在高度方向的缩放比例,越大地形看起来越陡峭。bRender设置使用多边形还是线绘制地形。  
   

#define  MAP_SIZE 1024    
#define  STEP_SIZE 16     // 相邻顶点的距离
#define  HEIGHT_RATIO 1.5f    
bool  bRender = TRUE;     // true为多边形渲染,false为线渲染

   
下面的代码用来保存高度数据  
   

BYTE g_HeightMap[MAP_SIZE*MAP_SIZE];    // 保存高度数据

float scaleValue = 0.15f;     // 地形的缩放比例

   
下面的函数从文件中加载高度数据  
   

// 从*.raw文件中加载高度数据
void LoadRawFile(LPSTR strName, int nSize, BYTE *pHeightMap)
{
 FILE *pFile = NULL;

 // 打开文件
 pFile = fopen( strName, "rb" );

 // 如果文件不能打开
 if ( pFile == NULL )
 {
  // 提示错误,退出
  MessageBox(NULL, "不能打开高度图文件", "错误", MB_OK);
  return;
 }

 // 读取文件数据到pHeightMap数组中
 fread( pHeightMap, 1, nSize, pFile );

 // 读取是否成功
 int result = ferror( pFile );

 // 如果不成功,提示错误退出
 if (result)
 {
  MessageBox(NULL, "读取数据失败", "错误", MB_OK);
 }

 // 关闭文件
 fclose(pFile);
}

   
InitGL函数基本没有变化,只是加入了加载高度图的函数  
   

// 载入1024*1024的高度图道g_HeightMap数组中

 LoadRawFile("Data/Terrain.raw", MAP_SIZE * MAP_SIZE, g_HeightMap);
   
下面的函数返回(x,y)点的高度  
   

int Height(BYTE *pHeightMap, int X, int Y)   // 下面的函数返回(x,y)点的高度
{
 int x = X % MAP_SIZE;    // 限制X的值在0-1024之间
 int y = Y % MAP_SIZE;    // 限制Y的值在0-1024之间

 if(!pHeightMap) return 0;    // 检测高度图是否存在,不存在则返回0

   
返回(x,y)的高度  
   

 return pHeightMap[x + (y * MAP_SIZE)];   // 返回(x,y)的高度
}

   
按高度设置顶点的颜色,越高的地方越亮  
   

void SetVertexColor(BYTE *pHeightMap, int x, int y)   // 按高度设置顶点的颜色,越高的地方越亮
{        
 if(!pHeightMap) return;     

 float fColor = -0.15f + (Height(pHeightMap, x, y ) / 256.0f);

 // 设置顶点的颜色
 glColor3f(0.0f, 0.0f, fColor );
}

   
下面的函数在OpenGL中,根据高度图渲染输出地形  
   

void RenderHeightMap(BYTE pHeightMap[])    // 根据高度图渲染输出地形
{
 int X = 0, Y = 0;      // 设置循环变量
 int x, y, z;      

 if(!pHeightMap) return;     // 确认高度图存在

 if(bRender)      // 选择渲染模式
  glBegin( GL_QUADS );    // 渲染为四边形
 else
  glBegin( GL_LINES );    // 渲染为直线


   
下面的函数求得每一点的坐标和颜色,调用OpenGL渲染  
   

 for ( X = 0; X < MAP_SIZE; X += STEP_SIZE )
  for ( Y = 0; Y < MAP_SIZE; Y += STEP_SIZE )
  {
   // 绘制(x,y)处的顶点
 // 获得(x,y,z)坐标
   x = X;
   y = Height(pHeightMap, X, Y );
   z = Y;

   // 设置顶点颜色
   SetVertexColor(pHeightMap, x, z);

   glVertex3i(x, y, z);   // 调用OpenGL绘制顶点的命令

   // 绘制(x,y+1)处的顶点
   x = X;
   y = Height(pHeightMap, X, Y + STEP_SIZE );
   z = Y + STEP_SIZE ;
   SetVertexColor(pHeightMap, x, z);
   glVertex3i(x, y, z);  

 // 绘制(x+1,y+1)处的顶点
   x = X + STEP_SIZE;
   y = Height(pHeightMap, X + STEP_SIZE, Y + STEP_SIZE );
   z = Y + STEP_SIZE ;
   SetVertexColor(pHeightMap, x, z);
   glVertex3i(x, y, z);   

   // 绘制(x+1,y)处的顶点
   x = X + STEP_SIZE;
   y = Height(pHeightMap, X + STEP_SIZE, Y );
   z = Y;
   SetVertexColor(pHeightMap, x, z);
   glVertex3i(x, y, z);  
  }
 glEnd();

 glColor4f(1.0f, 1.0f, 1.0f, 1.0f);   // 重置颜色
}

   
DrawGLScene函数基本没变化,只是设置了视点和缩放系数,调用上面的函数绘制出地形。   
   

// 设置视点
 gluLookAt(212, 60, 194,  186, 55, 171,  0, 1, 0); 
 glScalef(scaleValue, scaleValue * HEIGHT_RATIO, scaleValue);  

 RenderHeightMap(g_HeightMap);    // 渲染高度图

 return TRUE;   
}

   
WndProc()函数基本没有变化,只是加入了单击左键的相应函数   
   

  case WM_LBUTTONDOWN:    // 是否单击鼠标左键
  {
   bRender = !bRender;   // 改变渲染模式
   return 0;     // 返回
  }

   
上面就是所有绘制地形的代码了,简单吧。
希望你喜欢这个教程!



--  作者:一分之千
--  发布时间:10/24/2007 4:54:00 PM

--  
Lesson 34
   
Welcome to another exciting tutorial! The code for this tutorial was written by Ben Humphrey, and is based on the GL framework from lesson 1. By now you should be a GL expert {grin}, and moving the code into your own base code should be a snap!

This tutorial will teach you how to create cool looking terrain from a height map. For those of you that have no idea what a height map is, I will attempt a crude explanation. A height map is simply... displacement from a surface. For those of you that are still scratching your heads asking yourself "what the heck is this guy talking about!?!"... In english, our heightmap represents low and height points for our landscape. It's completely up to you to decide which shades represent low points and which shades represent high points. It's also important to note that height maps do not have to be images... you can create a height map from just about any type of data. For instance, you could use an audio stream to create a visual height map representation. If you're still confused... keep reading... it will all start to make sense as you go through the tutorial :)   
   

#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

#pragma comment(lib, "opengl32.lib")    // Link OpenGL32.lib
#pragma comment(lib, "glu32.lib")    // Link Glu32.lib

   
We start off by defining a few important variables. MAP_SIZE is the dimension of our map. In this tutorial, the map is 1024x1024. The STEP_SIZE is the size of each quad we use to draw the landscape. By reducing the step size, the landscape becomes smoother. It's important to note that the smaller the step size, the more of a performance hit your program will take, especially when using large height maps. The HEIGHT_RATIO is used to scale the landscape on the y-axis. A low HEIGHT_RATIO produces flatter mountains. A high HEIGHT_RATIO produces taller / more defined mountains.

Further down in the code you will notice bRender. If bRender is set to true (which it is by default), we will drawn solid polygons. If bRender is set to false, we will draw the landscape in wire frame.   
   

#define  MAP_SIZE 1024    // Size Of Our .RAW Height Map ( NEW )
#define  STEP_SIZE 16    // Width And Height Of Each Quad ( NEW )
#define  HEIGHT_RATIO 1.5f    // Ratio That The Y Is Scaled According To The X And Z ( NEW )

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 TRUE By Default
bool  bRender = TRUE;     // Polygon Flag Set To TRUE By Default ( NEW )

   
Here we make an array (g_HeightMap[ ]) of bytes to hold our height map data. Since we are reading in a .RAW file that just stores values from 0 to 255, we can use the values as height values, with 255 being the highest point, and 0 being the lowest point. We also create a variable called scaleValue for scaling the entire scene. This gives the user the ability to zoom in and out.   
   

BYTE g_HeightMap[MAP_SIZE*MAP_SIZE];    // Holds The Height Map Data ( NEW )

float scaleValue = 0.15f;     // Scale Value For The Terrain ( NEW )

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);  // Declaration For WndProc

   
The ReSizeGLScene() code is the same as lesson 1 except the farthest distance has been changed from 100.0f to 500.0f.   
   

GLvoid ReSizeGLScene(GLsizei width, GLsizei height)  // Resize And Initialize The GL Window
{
... CUT ...
}

   
The following code loads in the .RAW file. Not too complex! We open the file in Read/Binary mode. We then check to make sure the file was found and that it could be opened. If there was a problem opening the file for whatever reason, an error message will be displayed.   
   

// Loads The .RAW File And Stores It In pHeightMap
void LoadRawFile(LPSTR strName, int nSize, BYTE *pHeightMap)
{
 FILE *pFile = NULL;

 // Open The File In Read / Binary Mode.
 pFile = fopen( strName, "rb" );

 // Check To See If We Found The File And Could Open It
 if ( pFile == NULL )
 {
  // Display Error Message And Stop The Function
  MessageBox(NULL, "Can't Find The Height Map!", "Error", MB_OK);
  return;
 }

   
If we've gotten this far, then it's safe to assume there were no problems opening the file. With the file open, we can now read in the data. We do this with fread(). pHeightMap is the storage location for the data (pointer to our g_Heightmap array). 1 is the number of items to load (1 byte at a time), nSize is the maximum number of items to read (the image size in bytes - width of image * height of image). Finally, pFile is a pointer to our file structure!

After reading in the data, we check to see if there were any errors. We store the results in result and then check result. If an error did occur, we pop up an error message.

The last thing we do is close the file with fclose(pFile).   
   

 // Here We Load The .RAW File Into Our pHeightMap Data Array
 // We Are Only Reading In '1', And The Size Is (Width * Height)
 fread( pHeightMap, 1, nSize, pFile );

 // After We Read The Data, It's A Good Idea To Check If Everything Read Fine
 int result = ferror( pFile );

 // Check If We Received An Error
 if (result)
 {
  MessageBox(NULL, "Failed To Get Data!", "Error", MB_OK);
 }

 // Close The File
 fclose(pFile);
}

   
The init code is pretty basic. We set the background clear color to black, set up depth testing, polygon smoothing, etc. After doing all that, we load in our .RAW file. To do this, we pass the filename ("Data/Terrain.raw"), the dimensions of the .RAW file (MAP_SIZE * MAP_SIZE) and finally our HeightMap array (g_HeightMap) to LoadRawFile(). This will jump to the .RAW loading code above. The .RAW file will be loaded, and the data will be stored in our Heightmap array (g_HeightMap).   
   

int InitGL(GLvoid)      // All Setup For OpenGL Goes Here
{
 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

 // Here we read read in the height map from the .raw file and put it in our
 // g_HeightMap array.  We also pass in the size of the .raw file (1024).

 LoadRawFile("Data/Terrain.raw", MAP_SIZE * MAP_SIZE, g_HeightMap); // ( NEW )

 return TRUE;      // Initialization Went OK
}

   
This is used to index into our height map array. When ever we are dealing with arrays, we want to make sure that we don't go outside of them. To make sure that doesn't happen we use %. % will prevent our x / y values from exceeding MAX_SIZE - 1.

We check to make sure pHeightMap points to valid data, if not, we return 0.

Otherwise, we return the value stored at x, y in our height map. By now, you should know that we have to multiply y by the width of the image MAP_SIZE to move through the data. More on this below!   
   

int Height(BYTE *pHeightMap, int X, int Y)   // This Returns The Height From A Height Map Index
{
 int x = X % MAP_SIZE;     // Error Check Our x Value
 int y = Y % MAP_SIZE;     // Error Check Our y Value

 if(!pHeightMap) return 0;    // Make Sure Our Data Is Valid

   
We need to treat the single array like a 2D array. We can use the equation: index = (x + (y * arrayWidth) ). This is assuming we are visualizing it like: pHeightMap[x][y], otherwise it's the opposite: (y + (x * arrayWidth) ).

Now that we have the correct index, we will return the height at that index (data at x, y in our array).   
   

 return pHeightMap[x + (y * MAP_SIZE)];   // Index Into Our Height Array And Return The Height
}

   
Here we set the color for a vertex based on the height index. To make it darker, I start with -0.15f. We also get a ratio of the color from 0.0f to 1.0f by dividing the height by 256.0f. If there is no data this function returns without setting the color. If everything goes ok, we set the color to a shade of blue using glColor3f(0.0f, fColor, 0.0f). Try moving fColor to the red or green spots to change the color of the landscape.   
   

void SetVertexColor(BYTE *pHeightMap, int x, int y)  // This Sets The Color Value For A Particular Index
{        // Depending On The Height Index
 if(!pHeightMap) return;     // Make Sure Our Height Data Is Valid

 float fColor = -0.15f + (Height(pHeightMap, x, y ) / 256.0f);

 // Assign This Blue Shade To The Current Vertex
 glColor3f(0.0f, 0.0f, fColor );
}

   
This is the code that actually draws our landscape. X and Y will be used to loop through the height map data. x, y and z will be used to render the quads making up the landscape.

As always, we check to see if the height map (pHeightMap) contains data. If not, we return without doing anything.   
   

void RenderHeightMap(BYTE pHeightMap[])    // This Renders The Height Map As Quads
{
 int X = 0, Y = 0;     // Create Some Variables To Walk The Array With.
 int x, y, z;      // Create Some Variables For Readability

 if(!pHeightMap) return;     // Make Sure Our Height Data Is Valid

   
Since we can switch between lines and quads, we check our render state with the code below. If bRender = True, then we want to render polygons, otherwise we render lines.   
   

 if(bRender)      // What We Want To Render
  glBegin( GL_QUADS );    // Render Polygons
 else
  glBegin( GL_LINES );    // Render Lines Instead


   
Next we actually need to draw the terrain from the height map. To do that, we just walk the array of height data and pluck out some heights to plot our points. If we could see this happening, it would draw the columns first (Y), then draw the rows. Notice that we have a STEP_SIZE. This determines how defined our height map is. The higher the STEP_SIZE, the more blocky the terrain looks, while the lower it gets, the more rounded (smooth) it becomes. If we set STEP_SIZE = 1 it would create a vertex for every pixel in the height map. I chose 16 as a decent size. Anything too much less gets to be insane and slow. Of course, you can increase the number when you get lighting in. Then vertex lighting would cover up the blocky shape. Instead of lighting, we just put a color value associated with every poly to simplify the tutorial. The higher the polygon, the brighter the color is.   
   

 for ( X = 0; X < MAP_SIZE; X += STEP_SIZE )
  for ( Y = 0; Y < MAP_SIZE; Y += STEP_SIZE )
  {
   // Get The (X, Y, Z) Value For The Bottom Left Vertex
   x = X;
   y = Height(pHeightMap, X, Y );
   z = Y;

   // Set The Color Value Of The Current Vertex
   SetVertexColor(pHeightMap, x, z);

   glVertex3i(x, y, z);   // Send This Vertex To OpenGL To Be Rendered

   // Get The (X, Y, Z) Value For The Top Left Vertex
   x = X;
   y = Height(pHeightMap, X, Y + STEP_SIZE );
   z = Y + STEP_SIZE ;

   // Set The Color Value Of The Current Vertex
   SetVertexColor(pHeightMap, x, z);

   glVertex3i(x, y, z);   // Send This Vertex To OpenGL To Be Rendered

   // Get The (X, Y, Z) Value For The Top Right Vertex
   x = X + STEP_SIZE;
   y = Height(pHeightMap, X + STEP_SIZE, Y + STEP_SIZE );
   z = Y + STEP_SIZE ;

   // Set The Color Value Of The Current Vertex
   SetVertexColor(pHeightMap, x, z);

   glVertex3i(x, y, z);   // Send This Vertex To OpenGL To Be Rendered

   // Get The (X, Y, Z) Value For The Bottom Right Vertex
   x = X + STEP_SIZE;
   y = Height(pHeightMap, X + STEP_SIZE, Y );
   z = Y;

   // Set The Color Value Of The Current Vertex
   SetVertexColor(pHeightMap, x, z);

   glVertex3i(x, y, z);   // Send This Vertex To OpenGL To Be Rendered
  }
 glEnd();

   
After we are done, we set the color back to bright white with an alpha value of 1.0f. If there were other objects on the screen, we wouldn't want them showing up BLUE :)   
   

 glColor4f(1.0f, 1.0f, 1.0f, 1.0f);   // Reset The Color
}

   
For those of you who haven't used gluLookAt(), what it does is position your camera position, your view, and your up vector. Here we set the camera in a obscure position to get a good outside view of the terrain. In order to avoid using such high numbers, we would divide the terrain's vertices by a scale constant, like we do in glScalef() below.

The values of gluLookAt() are as follows: The first three numbers represent where the camera is positioned. So the first three values move the camera 212 units on the x-axis, 60 units on the y-axis and 194 units on the z-axis from our center point. The next 3 values represent where we want the camera to look. In this tutorial, you will notice while running the demo that we are looking a little to the left. We are also look down towards the landscape. 186 is to the left of 212 which gives us the look to the left, and 55 is lower than 60, which gives us the appearance that we are higher than the landscape looking at it with a slight tilt (seeing a bit of the top of it). The value of 171 is how far away from the camera the object is. The last three values tell OpenGL which direction represents up. Our mountains travel upwards on the y-axis, so we set the value on the y-axis to 1. The other two values are set at 0.

gluLookAt can be very intimidating when you first use it. After reading the rough explanation above you may still be confused. My best advise is to play around with the values. Change the camera position. If you were to change the y position of the camera to say 120, you would see more of the top of the landscape, because you would be looking all the way down to 55.

I'm not sure if this will help, but I'm going to break into one of my highly flamed real life "example" explanations :) Lets say you are 6 feet and a bit tall. Lets also assume your eyes are at the 6 foot mark (your eyes represent the camera - 6 foot is 6 units on the y-axis). Now if you were standing in front of a wall that was only 2 feet tall (2 units on the y-axis), you would be looking DOWN at the wall and would be able to see the top of the wall. If the wall was 8 feet tall, you would be looking UP at the wall and you would NOT see the top of the wall. The view would change depending on if you were looking up or down (if you were higher than or lower than the object you are looking at). Hope that makes a bit of sense!   
   

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 Matrix

 //    Position  View  Up Vector
 gluLookAt(212, 60, 194,  186, 55, 171,  0, 1, 0); // This Determines The Camera's Position And View

   
This will scale down our terrain so it's a bit easier to view and not so big. We can change this scaleValue by using the UP and DOWN arrows on the keyboard. You will notice that we mupltiply the Y scaleValue by a HEIGHT_RATIO as well. This is so the terrain appears higher and gives it more definition.   
   

 glScalef(scaleValue, scaleValue * HEIGHT_RATIO, scaleValue);

   
If we pass the g_HeightMap data into our RenderHeightMap() function it will render the terrain in Quads. If you are going to make any use of this function, it might be a good idea to put in an (X, Y) parameter to draw it at, or just use OpenGL's matrix operations (glTranslatef() glRotate(), etc) to position the land exactly where you want it.   
   

 RenderHeightMap(g_HeightMap);    // Render The Height Map

 return TRUE;      // Keep Going
}

   
The KillGLWindow() code is the same as lesson 1.   
   

GLvoid KillGLWindow(GLvoid)     // Properly Kill The Window
{
}

   
The CreateGLWindow() code is also the same as lesson 1.   
   

BOOL CreateGLWindow(char* title, int width, int height, int bits, bool fullscreenflag)
{
}

   
The only change in WndProc() is the addition of WM_LBUTTONDOWN. What it does is checks to see if the left mouse button was pressed. If it was, the rendering state is toggled from polygon mode to line mode, or from line mode to polygon mode.   
   

LRESULT CALLBACK WndProc( HWND hWnd,   // Handle For This Window
    UINT uMsg,   // Message For This Window
    WPARAM wParam,   // Additional Message Information
    LPARAM lParam)   // Additional Message Information
{
 switch (uMsg)      // Check For Windows Messages
 {
  case WM_ACTIVATE:    // Watch For Window Activate Message
  {
   if (!HIWORD(wParam))   // Check Minimization State
   {
    active=TRUE;   // Program Is Active
   }
   else
   {
    active=FALSE;   // Program Is No Longer Active
   }

   return 0;    // Return To The Message Loop
  }

  case WM_SYSCOMMAND:    // Intercept System Commands
  {
   switch (wParam)    // Check System Calls
   {
    case SC_SCREENSAVE:  // Screensaver Trying To Start?
    case SC_MONITORPOWER:  // Monitor Trying To Enter Powersave?
    return 0;   // Prevent From Happening
   }
   break;     // Exit
  }

  case WM_CLOSE:     // Did We Receive A Close Message?
  {
   PostQuitMessage(0);   // Send A Quit Message
   return 0;    // Jump Back
  }

  case WM_LBUTTONDOWN:    // Did We Receive A Left Mouse Click?
  {
   bRender = !bRender;   // Change Rendering State Between Fill/Wire Frame
   return 0;    // Jump Back
  }

  case WM_KEYDOWN:    // Is A Key Being Held Down?
  {
   keys[wParam] = TRUE;   // If So, Mark It As TRUE
   return 0;    // Jump Back
  }

  case WM_KEYUP:     // Has A Key Been Released?
  {
   keys[wParam] = FALSE;   // If So, Mark It As FALSE
   return 0;    // Jump Back
  }

  case WM_SIZE:     // Resize The OpenGL Window
  {
   ReSizeGLScene(LOWORD(lParam),HIWORD(lParam)); // LoWord=Width, HiWord=Height
   return 0;    // Jump Back
  }
 }

 // Pass All Unhandled Messages To DefWindowProc
 return DefWindowProc(hWnd,uMsg,wParam,lParam);
}

   
No major changes in this section of code. The only notable change is the title of the window. Everything else is the same up until we check for key presses.   
   

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
 }

 // Create Our OpenGL Window
 if (!CreateGLWindow("NeHe & Ben Humphrey's Height Map 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 if (active)   // Not Time To Quit, Update Screen
   {
    SwapBuffers(hDC);  // Swap Buffers (Double Buffering)
   }

   if (keys[VK_F1])   // Is F1 Being Pressed?
   {
    keys[VK_F1]=FALSE;  // If So Make Key FALSE
    KillGLWindow();   // Kill Our Current Window
    fullscreen=!fullscreen;  // Toggle Fullscreen / Windowed Mode
    // Recreate Our OpenGL Window
    if (!CreateGLWindow("NeHe & Ben Humphrey's Height Map Tutorial", 640, 480, 16, fullscreen))
    {
     return 0;  // Quit If Window Was Not Created
    }
   }

   
The code below lets you increase and decrease the scaleValue. By pressing the up key, the scaleValue is increased, making the landscape larger. By pressing the down key, the scaleValue is decreased making the landscape smaller.   
   

   if (keys[VK_UP])   // Is The UP ARROW Being Pressed?
    scaleValue += 0.001f;  // Increase The Scale Value To Zoom In

   if (keys[VK_DOWN])   // Is The DOWN ARROW Being Pressed?
    scaleValue -= 0.001f;  // Decrease The Scale Value To Zoom Out
  }
 }

 // Shutdown
 KillGLWindow();      // Kill The Window
 return (msg.wParam);     // Exit The Program
}

   
That's all there is to creating a beautiful height mapped landscape. I hope you appreciate Ben's work! As always, if you find mistakes in the tutorial or the code, please email me, and I will attempt to correct the problem / revise the tutorial.

Once you understand how the code works, play around a little. One thing you could try doing is adding a little ball that rolls across the surface. You already know the height of each section of the landscape, so adding the ball should be no problem. Other things to try: Create the heightmap manually, make it a scrolling landscape, add colors to the landscape to represent snowy peaks / water / etc, add textures, use a plasma effect to create a constantly changing landscape. The possibilities are endless :)

Hope you enjoyed the tut! You can visit Ben's site at: http://www.GameTutorials.com.

Ben Humphrey (DigiBen)

Jeff Molofee (NeHe)


W 3 C h i n a ( since 2003 ) 旗 下 站 点
苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》
191.406ms