|
|
Nehe 36课 在OpenGL中播放AVI文件
freefall
在开始的时候我应该说我对这篇教程感到自豪。是Jonathan让我有了写一个OpenGL中的AVI播放器的想法。开始的时候我翻遍了我所有的编程书籍,但没有一本谈到了AVI文件。于是我在MSDN中寻找关于AVI格式的资料,MSDN中有许多的有用信息,但是我需要更多的信息。
在网上找了几个小时的AVI例子之后,我仅发现了两个关于这方面的网站。我不是说我使用搜索引擎的能力很惊人,但是99.9%的时间我都能够找到我所需要的。在我意识到网上AVI例子是很少的时候我觉得很震惊!我找到的大多数的例子都是不能够编译的… 它们中的很多都是很复杂的(至少对我来说是这样的),剩下的能够运行,但它们都是用VB , Delphi,等写的(不是VC++)。
我找到的第一篇是Jonathan Nix写的名为"AVI Files"的文章。你能够在http://www.gamedev.net/reference/programming/features/avifile/上找到。对于Jonathan写的关于AVI文件格式的文档给予极高评价。虽然我决定以不同的方法来完成它,但它的例子的代码段和清楚的注释使得学习过程变得更加容易!另一个网站是John F. McGowan, Ph .D的"The AVI Overview"。我不断的感叹他的网页的惊人之处,如果你自己看一遍你会觉得更加容易的!网址是http://www.jmcgowan.com/avi.htm。他的网站几乎包含了关于AVI文件的所有内容!感谢John为大家写了如此有价值的文章。
最后,我想要提到的是我没有借鉴和拷贝别人的代码。我疯狂的写了3天的代码,利用了上面提到的网站和文章。我的代码并不是播放AVI文件的最好的方法,甚至不是正确的方法,但它的确能够工作!如果你不喜欢这些代码、编码风格 或是我发表这篇教程伤害了编程组织,你可以这样:1)在网上寻找替代资源。2)写你自己的AVI播放器。或是 3)写一个更好的教程!每个访问这个网站的人都应该知道我只是一个有普通技能的普通的程序员(我的很多教程都以这句话作为开始)!我编码只是为了消遣!这个网站的目的是使得普通的程序员在刚开始学习OpenGL的时候更加容易。这个教程仅仅是一些关于怎样完成某种特效的例子… 不多也不少!
来看看代码…
你会注意到的第一件事就是我们包含和连接了Windows的视频头文件/库。非常感谢微软公司(我都不敢相信我刚才说的!)。这个库使得打开和播放AVI文件很容易!现在…你所需的是<如果你想要编译你的代码,你就应该包含vfw.h 并且 连接 vfw32.lib库文件>
#include <windows.h> // Header File For Windows
#include <gl\gl.h> // Header File For The OpenGL32 Library
#include <gl\glu.h> // Header File For The GLu32 Library
#include <vfw.h> // Header File For Video For Windows
#include "NeHeGL.h" // Header File For NeHeGL
#pragma comment( lib, "opengl32.lib" ) // Search For OpenGL32.lib While Linking
#pragma comment( lib, "glu32.lib" ) // Search For GLu32.lib While Linking
#pragma comment( lib, "vfw32.lib" ) // Search For VFW32.lib While Linking
#ifndef CDS_FULLSCREEN // CDS_FULLSCREEN Is Not Defined By Some
#define CDS_FULLSCREEN 4 // Compilers. By Defining It This Way,
#endif // We Can Avoid Errors
GL_Window* g_window;
Keys* g_keys;
现在我们来定义我们的变量。Angle是用来旋转我们物体围绕基于经过的时间(angle is used to rotate our objects around based on the amount of time that has passed),我们对旋转使用角度仅仅是为了让它更加简单。
接下来是一个整形变量,它是用来计数经过了多少时间(使用毫秒)。它是用来保持一个比较好的帧速。后边我们会更多的谈到!
Frame是我们要显示的动画的当前帧。我们从0开始(第一帧)。我们已经可以认为我们成功的打开了视频了,一个动画至少有一帧。
效果就是在屏幕上看到的当前效果(物体:方块、圆、圆柱、什么也没有)。Env是一个布尔值如果它的值为真,那么环境绘制就被设置为有效,如果为假,这个物体的环境就不会被绘制,如果bg的值为真,你就会看到视频在物体后方全屏播放,如果它的值为假,你就只会看到物体(没有背景)。
Sp,ep和bp用来确定使用着没有按下了一个键。
// User Defined Variables
float angle; // Used For Rotation
int next; // Used For Animation
int frame=0; // Frame Counter
int effect; // Current Effect
bool sp; // Space Bar Pressed?
bool env=TRUE; // Environment Mapping (Default On)
bool ep; // 'E' Pressed?
bool bg=TRUE; // Background (Default On)
bool bp; // 'B' Pressed?
Psi结构体存的是后边代码中的AVI文件的信息,Pavi是一个指向一个缓冲区,这个缓冲区中存放的是AVI文件打开时的新的流的句柄。Pfg是指向得到帧的物体(GetFrame object),
Bmih会在后边把动画中的帧转换成我们所需要的格式的代码中被用到(保存了描述我们所需要的信息的位图头部)。Lastframe会保存AVI动画的上一帧的数值。Width 和height 保存了AVI流的大小,并且最后… pdata是指向我们从AVI动画中的帧中得到的数据的指针!mpf会被用来计算显示每帧所需的的毫秒数。更多的会在下边讨论。
AVISTREAMINFO psi; // Pointer To A Structure Containing Stream Info
PAVISTREAM pavi; // Handle To An Open Stream
PGETFRAME pgf; // Pointer To A GetFrame Object
BITMAPINFOHEADER bmih; // Header Information For DrawDibDraw Decoding
long lastframe; // Last Frame Of The Stream
int width; // Video Width
int height; // Video Height
char *pdata; // Pointer To Texture Data
int mpf; // Will Hold Rough Milliseconds Per Frame
在这篇教程中我们使用GLU库创建了两个不同的二次曲面形(一个球和一个圆柱)。Guadratic是一个指向我们的二次曲面物体的指针。Hdd是一个DrawDib设备上下文句柄。Hdc是一个设备上下文句柄。hBitmap是设备无关位图句柄(用在后面的位图转换处理)。Data是一个真正指向我们转换后的位图数据,将会在后面发挥作用。接着往后读。
GLUquadricObj *quadratic; // Storage For Our Quadratic Objects
HDRAWDIB hdd; // Handle For Our Dib
HBITMAP hBitmap; // Handle To A Device Dependant Bitmap
HDC hdc = CreateCompatibleDC(0); // Creates A Compatible Device Context
unsigned char* data = 0; // Pointer To Our Resized Image
现在要用到一些汇编语言了。对于那些从没用过汇编的人不要被吓住了。它们看起来很神秘,其实是很简单的!
在写这篇教程的时候,我发现了一些很奇怪的事。我使用这段代码的第一个视频工作得非常好,只是颜色很混乱。那些应该是红色的东西全都是蓝色,那些本应该是蓝色的东西都是红色的。我都快要疯了!我深信在程序中有错误。在通读了一遍代码后我还是没能找到错误!于是我又从新开始读MSDN。为什么红色和蓝色交换了呢!?MSDN中说24位的位图用的是RGB!!!在阅读了更多的以后我发现了这个错误。在windows中(图象),数据实际上是以相反的顺序存储的(BGR)。在OpenGL中,RGB就是真正的RGB!
在微软的爱好者的抱怨下,我决定加一些注释!我不会认为微软是垃圾仅仅因为他把RGB数据以相反的顺序存储,我只是觉得很沮丧,因为名字叫RGB而在文件中却是BGR!
Blue 还增加了一些:
好极了!现在我和一个表演者在一起,看起来就象是个废物!我的第一个解决办法是设置一个循环手工的交换这些字节。它起作用了,但是很慢。完全厌烦了,我把纹理产生的代码修改为使用GL_BGR_EXT而不是GL_RGB。速度有了很大的提高,颜色看起来也不错!所以我的问题解决了…或者我是这样想的!但是问题出来了,某些OpenGL驱动不能使用GL_BGR_EXT …不得不返回绘制平台。
在和我的好朋友Maxwell Sayles谈了以后,他建议我用汇编代码转换这些字节。一分钟后,他用ICQ把下面这些代码发给了我!它没有优化,但它很快而且很有效!
动画中的每一帧都存储在缓冲区中。图片总是要256个像素宽,256个像素高,每种颜色一个字节(每个像素3个字节)。下面的代码遍历了整个缓冲区并且交换了红色和蓝色的字节。红色存在ebx+0,蓝色存在ebx+2处。我们每次移动3个字节(每个像素是由3个字节组成的)。我们循环操作所有数据,直到所有的字节都被交换完毕。
有些人会对我使用汇编代码表示不满意,所以我指出我为什么在这篇教程中使用它的原因。首先,我开始的时候计划使用GL_BGR_EXT,它是有效的,但不是在所有的显卡都支持!然后我决定使用上篇教程中的交换方法(非常工整的通过异或的交换代码)。这段交换代码在所有的电脑上都能工作,但是速度并不快。在上篇教程中它工作得很好。在这篇教程中我们要处理的是实时的视频。你想要的就是你能得到的最高速度的交换。衡量了这些选择,在我看来汇编是一种最好的选择!如果你还有其它更好的方法来完成这项工作,请…使用它!我不是要告诉你你必须怎么做。我只是给你展示我是这样完成的。我也会详细的解释这段代码所做的工作。这样的话如果你想把这段代码换成某些更好的,你就会知道这段代码究竟在做什么,如果你要写你自己的代码的话,你就能更容易的找到替代的解决办法!
void flipIt(void* buffer) // Flips The Red And Blue Bytes (256x256)
{
void* b = buffer; // Pointer To The Buffer
__asm // Assembler Code To Follow
{
mov ecx, 256*256 // Set Up A Counter (Dimensions Of Memory Block)
mov ebx, b // Points ebx To Our Data (b)
label: // Label Used For Looping
mov al,[ebx+0] // Loads Value At ebx Into al
mov ah,[ebx+2] // Loads Value At ebx+2 Into ah
mov [ebx+2],al // Stores Value In al At ebx+2
mov [ebx+0],ah // Stores Value In ah At ebx
add ebx,3 // Moves Through The Data By 3 Bytes
dec ecx // Decreases Our Loop Counter
jnz label // If Not Zero Jump Back To Label
}
}
下面这段代码以读的方式打开了一个AVI文件。szFile是我们要打开文件的名称。Title[100]会被用来改变窗体的标题(来显示AVI文件的一些信息)。
我们要做的第一件事是调用AVIFileInit()函数,它的作用是初始化AVI文件库(使一切都准备好)。
有许多打开AVI文件的方法。我决定使用AVIStreamOpenFromFile(...)函数,它把AVI文件以一个单一的流打开(AVI文件可以包含多个流)。
参数是以下这些:Pavi是指向一个用于存放新的流的句柄。szFile当然是我们想要打开的文件的文件名(绝对路径)。第三个参数是我们想要打开的流的类型。在这个工程中,我们仅仅对视频流感兴趣(streamtypeVIDEO)。第四个参数是0,它代表我们要的是第一条出现的视频流(一个AVI文件中可以有多条视频流,我们只要第一条)。OF_READ意味着我们打开文件是只读的。最后一个参数是一个指向你想要用的句柄的类标志符号。说句实话,我不知道它是什么意思。我只是传递了一个NULL做为参数,让windows为我选了一个。
如果在打开文件的时候出现错误,就会弹出一个消息框让你知道这个流不能被打开。我没有传递一个PASS或是FAIL给调用这部分的代码,所以如果打开文件失败了,程序仍然会继续运行。增加一些错误检查不会花太多的工夫的,我只是太懒了。
void OpenAVI(LPCSTR szFile) // Opens An AVI File (szFile)
{
TCHAR title[100]; // Will Hold The Modified Window Title
AVIFileInit(); // Opens The AVIFile Library
// Opens The AVI Stream
if (AVIStreamOpenFromFile(&pavi, szFile, streamtypeVIDEO, 0, OF_READ, NULL) !=0)
{
// An Error Occurred Opening The Stream
MessageBox (HWND_DESKTOP, "Failed To Open The AVI Stream", "Error", MB_OK | MB_ICONEXCLAMATION);
}
到这里我们已经可以假设文件被打开并且一条流已经被定位了!下一不我们要从AVI文件中取得一些信息,我们可以使用AVIStreamInfo(...)。
在前边我们创建了一个名为psi的结构体用来保存我们的AVI流的信息。我们用下边的第一行代码使该结构填满信息。所有的信息从流的带宽(像素)到动画的帧速都保存在psi中了。对于那些想要精确的回放速度的人,请记住我刚刚所说的。想要得到更多的信息请查看MSDN中的AVIStreamInfo。
我们能够用左边的边界减去右边的边界来计算帧的宽度,结果是精确的用像素表示的宽度。对于高度,我们用帧的顶部减去帧的底部,来得到用像素表示的高度。
然后我们用函数AVIStreamLength(...)来得到最后一帧的编号。这个函数返回AVI动画的帧数。结果存储在lastframe中。
计算帧速是很简单的。帧/秒= psi.dwRate / psi.dwScale。当你用右键点击AVI并检查它的属性的时候,返回的值应该与显示的帧速相匹配。这和你问的mpf有什么关系呢?当我第一次写动画代码的时候,我试着用每秒的帧速来选择正确的动化帧速。我遇到了一个问题…所有的视频播放得太快了!所以我看了看视频属性。face2.avi文件有3.36秒长。帧速是29.974帧每秒。视频有91帧动画。如果你将3.36乘以29.974你将得到100帧动画。非常奇怪!
所以 ,我决定采用另一种方法,不再计算每秒的帧速,而是计算显示每帧所需的时间。AVIStreamSampleToTime()函数把动画的位置转换成了要多少毫秒才能到达这个位置。所以我们通过得到运行到最后一帧的时间来计算整个视频的长度。然后把这个时间除以动画中的总帧数(lastframe),这样我们就得到了每帧运行所需要的毫秒数了。我们把结果保存在mpf (milliseconds per frame)中。你也可以通过计算单一帧的时间来得到每帧的时间,你可以使用这个函数AVIStreamSampleToTime(pavi,1)。两种方法都能工作得很好!非常 Albert Chaulk提供的方法!
我所它是粗略的毫秒数的原因是因为mpf是一个整型所以任何浮点值都会被省略掉。
AVIStreamInfo(pavi, &psi, sizeof(psi)); // Reads Information About The Stream Into psi
width=psi.rcFrame.right-psi.rcFrame.left; // Width Is Right Side Of Frame Minus Left
height=psi.rcFrame.bottom-psi.rcFrame.top; // Height Is Bottom Of Frame Minus Top
lastframe=AVIStreamLength(pavi); // The Last Frame Of The Stream
mpf=AVIStreamSampleToTime(pavi,lastframe)/lastframe; // Calculate Rough Milliseconds Per Frame
因为 OpenGL所需的纹理数据必须是2的次方,但是大多数视频都是160x120, 320x240 或其它一些奇怪的尺寸。我们需要一种快速的方法把格式转换成作为纹理所需的格式大小。要完成这项工作。我们利用了几个特定的Windows Dib函数。
首先我们需要做的是描述我们所需要的图片的类型。我们将bmih BitmapInfoHeader结构体中填满我们所需的参数。我们从设置结构体的大小开始。然后把bitplanes设为1。用3个字节的数据组成24位(RGB)。我们想要图片大小为256个像素宽和256个像素高,最后我们想让数据的返回类型为UNCOMPRESSED RGB data (BI_RGB)。
CreateDIBSection 创建了一个我们能直接写入的dib。如果一切正常的话,hBitmap会指向dib的位的值。Hdc是一个设备上下文句柄(DC)。第二个参数是一个指向BitmapInfo的结构体。这个结构包含了上边提到的dib文件的信息。第三个参数(DIB_RGB_COLORS)表明数据是RGB值。Data是一个指向一个变量的指针,这个变量接收了另一个指向DIB位的值的位置的指针。把5个参数被设置为NULL,就为我们的DIB分配了内存。最后一个参数可以被忽略(设置为NULL)。
从msdn的引用:The SelectObject function selects an object into the specified device context (DC)。
现在我们已经创建了一个我们能直接绘制的DIB。
bmih.biSize = sizeof (BITMAPINFOHEADER); // Size Of The BitmapInfoHeader
bmih.biPlanes = 1; // Bitplanes
bmih.biBitCount = 24; // Bits Format We Want (24 Bit, 3 Bytes)
bmih.biWidth = 256; // Width We Want (256 Pixels)
bmih.biHeight = 256; // Height We Want (256 Pixels)
bmih.biCompression = BI_RGB; // Requested Mode = RGB
hBitmap = CreateDIBSection (hdc, (BITMAPINFO*)(&bmih), DIB_RGB_COLORS, (void**)(&data), NULL, NULL);
SelectObject (hdc, hBitmap); // Select hBitmap Into Our Device Context (hdc)
在从AVI文件中读取帧之前还有一些事情要做。下一件事就是让程序准备从AVI文件中解压出视频帧。我们用AVIStreamGetFrameOpen(...)函数来完成这项工作。
你可以传递一个与上面相似的结构体作为第二个参数,返回一个特定的视频格式。很不幸,你能改变的唯一的东西只是返回图片的宽度和高度。MSDN上提到了可以传递 AVIGETFRAMEF_BESTDISPLAYFMT来选择最好的显示格式。很奇怪的是,我的编译器并没有为它定义。
如果一切正常的话,一个GETFRAME 对象将被返回(这就是我们需要读的帧数据)。如果发生了任何错误,一个提示框会弹出来告诉你发生了一个错误!
pgf=AVIStreamGetFrameOpen(pavi, NULL); // Create The PGETFRAME Using Our Request Mode
if (pgf==NULL)
{
// An Error Occurred Opening The Frame
MessageBox (HWND_DESKTOP, "Failed To Open The AVI Frame", "Error", MB_OK | MB_ICONEXCLAMATION);
}
下面的代码打印出了视频的宽度、高度和帧作为窗口的标题。我们用SetWindowText(...)在窗体顶部显示标题,在窗口模式下运行这个程序就能看见这些代码所做的工作。
// Information For The Title Bar (Width / Height / Last Frame)
wsprintf (title, "NeHe's AVI Player: Width: %d, Height: %d, Frames: %d", width, height, lastframe);
SetWindowText(g_window->hWnd, title); // Modify The Title Bar
}
现在该是一些有趣的东西了…我们从AVI文件中抓取了一帧并把它转换成一个可以使用的图片大小/颜色深度。Lpbi包含了动画中帧的BitmapInfoHeader的信息。用下面第二行的代码,我们很快的完成了一些事情。首先我们从动画中抓去了一帧…我们想要的这个帧是指定了的帧。这就要从动画中找到这一帧并把lpbi中填入这帧的头部信息。
现在该是一些有趣的东西了… 我们需要指向图片数据。要做到这点我们需要略过头部信息(lpbi->biSize)。还有一件我到开始写这篇教程的时候才意识到的事,我们还应该忽略各种颜色信息,所以我们需要通过乘以RGBQUAD的大小(biClrUsed*sizeof(RGBQUAD))来增加用过的颜色。把这些都做完了以后,我们就只剩下一个指向图片数据的指针了(pdata)。
现在我们需要把动画中的帧转换成能够使用的纹理大小,我们需要把数据转换成RGB数据。要完成这项工作,我们使用DrawDibDraw(...)函数。
一个简洁的解释,我们能直接绘制我们通常的DIB了,那就是DrawDibDraw(...)所做的工作了。第一个参数是DrawDib DC的句柄。第二个参数是DC的句柄。下面是目标矩形的左上角(0,0)和右下角(256,256)。
Pbi是一个指向我们刚读入的帧的bitmapinfoheader的信息的指针。Pdata是一个指向我们刚读入的帧的图象信息的指针。
然后我们有了源图片(我们刚刚读入)的左上角坐标(0,0)和刚读入帧的右下角坐标(帧的宽度,帧的高度)。最后的参数应该为图片左边离屏幕左的距离,设置为0。
这会把任意大小和颜色的图片转换成256*256大小,颜色为24bit的图片。
void GrabAVIFrame(int frame) // Grabs A Frame From The Stream
{
LPBITMAPINFOHEADER lpbi; // Holds The Bitmap Header Information
lpbi = (LPBITMAPINFOHEADER)AVIStreamGetFrame(pgf, frame); // Grab Data From The AVI Stream
pdata=(char *)lpbi+lpbi->biSize+lpbi->biClrUsed * sizeof(RGBQUAD); // Pointer To Data Returned By AVIStreamGetFrame
// (Skip The Header Info To Get To The Data)
// Convert Data To Requested Bitmap Format
DrawDibDraw (hdd, hdc, 0, 0, 256, 256, lpbi, pdata, 0, 0, width, height, 0);
我已经把动画帧的表示红色和表示蓝色字节给交换了。为了解决这个问题,我们跳到我们的快速的flipIt(...)代码。记住data是一个指向接收DIB值的位置的指针的指针。也就是说我们在调用DrawDibDraw 之后,data就会指向改变了大小的(256*256) / (24 bit)的位图数据。
刚开始更新纹理的时候我为动画的每一帧重新创建一个纹理。我收到了一些邮件建议我使用glTexSubImage2D()。在翻遍了红宝书之后,我感到困惑,有下面一段引用:”创建一个纹理比修改一个现有的纹理在计算上开销更大。在OpenGL 1.1中,有一段新的程序来替换全部或是部分纹理图片,这对有些程序很有帮助,比如利用实时捕获的视频图片作为纹理,对于那种程序,创建一个单一的纹理并使用glTexSubImage2D()不断的用新的视频图象来代替旧的图象做为纹理是很有意义的”。
我个人并没有注意到速度有多大的提高,但在慢点的显卡上你也许能注意到!glTexSubImage2D()的参数如下:我们的目标,是一个2D纹理(GL_TEXTURE_2D),细节度(0),使用mipmapping。X(0)和Y(0)偏移量告诉OpenGL从那里开始复制(0,0代表纹理的左下角)。接下来我们选择了我们想要复制的图片宽度,即256个像素宽和256个像素高。GL_RGB就是我们数据的格式。我们复制的是无符号位。最后…指向数据的指针(data)。非常简单。
Kevin Rogers补充到:我想要指出使用glTexSubImage2D的另一个原因。不仅仅是它在许多OpenGL上的实现很快,而且目标区域不必再是2的次方了。这对于视频播放是特别的便利,因为一帧的典型尺寸很少有2的次方的(通常都是象320×200)。这使得你从原来的角度上来看有了很大的便利,而不是扭曲/剪切每一帧来适应纹理尺寸。
有一点很重要,如果你没有在第一次创建一个纹理那么你是不能更新纹理的!我们在Initialize()代码中创建了一个纹理!
我还要提到的是…如果你打算在你的工程中使用一个以上的纹理,那么请确定你绑定的是你想要更新的纹理。如果你没有绑定这个纹理,那么你最后也许会更新你不想要更新的纹理!
flipIt(data); // Swap The Red And Blue Bytes (GL Compatability)
// Update The Texture
glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, 256, 256, GL_RGB, GL_UNSIGNED_BYTE, data);
}
下面这部分代码在程序退出时调用。我们关闭了DrawDib 设备上下文,并且释放了分配的资源。然后释放了AVI 的获得帧的资源。最后我们释放了数据流然后释放了文件资源。
void CloseAVI(void) // Properly Closes The Avi File
{
DeleteObject(hBitmap); // Delete The Device Dependant Bitmap Object
DrawDibClose(hdd); // Closes The DrawDib Device Context
AVIStreamGetFrameClose(pgf); // Deallocates The GetFrame Resources
AVIStreamRelease(pavi); // Release The Stream
AVIFileExit(); // Release The File
}
初始化是很工整简单的。我们把初始的angle设置为0。然后打开DrawDib库(它获得了一个设备上下文)。如果一切顺利的话,hdd就是新创建的设备上下文的句柄。
清屏的颜色设置为黑色,深度测试置为有效,等等。
我们创建了一个新的二次曲面。Quadratic是指向这个新物体的指针。我们设置了光滑的法线,然后为这个物体产生了纹理坐标。
BOOL Initialize (GL_Window* window, Keys* keys) // Any GL Init Code & User Initialiazation Goes Here
{
g_window = window;
g_keys = keys;
// Start Of User Initialization
angle = 0.0f; // Set Starting Angle To Zero
hdd = DrawDibOpen(); // Grab A Device Context For Our Dib
glClearColor (0.0f, 0.0f, 0.0f, 0.5f); // Black Background
glClearDepth (1.0f); // Depth Buffer Setup
glDepthFunc (GL_LEQUAL); // The Type Of Depth Testing (Less Or Equal)
glEnable(GL_DEPTH_TEST); // Enable Depth Testing
glShadeModel (GL_SMOOTH); // Select Smooth Shading
glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // Set Perspective Calculations To Most Accurate
quadratic=gluNewQuadric(); // Create A Pointer To The Quadric Object
gluQuadricNormals(quadratic, GLU_SMOOTH); // Create Smooth Normals
gluQuadricTexture(quadratic, GL_TRUE); // Create Texture Coords
在下一段代码中,我们打开了2D纹理贴图,我把文理滤波设置成GL_NEAREST(快速,看起来比较粗糙)并且设置了球的贴图(用来创建环境贴图效果)。如果你能的话,试着改变一下滤波方式,比如用GL_LINEAR来使动画看起来更加平滑。
在设置了我们的纹理和球体贴图后,我们打开.AVI文件。我努力让所有的东西看起来简单…我们要打开的文件名为face2.avi…它位于数据字典中。
我们所要做的最后一件事是创建我们的初始化纹理。我们这样做的原因是我们要使用glTexSubImage2D()和GrabAVIFrame()中得到的图片来更新我们的纹理。
glEnable(GL_TEXTURE_2D); // Enable Texture Mapping
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);// Set Texture Max Filter
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);// Set Texture Min Filter
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); // Set The Texture Generation Mode For S To Sphere Mapping
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); // Set The Texture Generation Mode For T To Sphere Mapping
OpenAVI("data/face2.avi"); // Open The AVI File
// Create The Texture
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
return TRUE; // Return TRUE (Initialization Successful)
}
当我们退出的时候,我们使用了CloseAVI()。这个函数完全的关闭了AVI文件,并且释放了所有用过的资源。
void Deinitialize (void) // Any User DeInitialization Goes Here
{
CloseAVI(); // Close The AVI File
}
这就是我们检查是否按键和更新旋转(angle)的地方,它是基于已经经过的时间的。到现在我已经没有必要再详细的解释代码了。我们检查空格键是否被按下。如果是的,我们就增加effect的值。我们有三种效果(方块、球、圆柱)当第四种效果被选中的时候(effect=3)什么也不会绘制…只是显示背景场景!如果我们在显示第四种效果并且空格键被按下,我们就设置为第一种效果(effect=0)。是的,我知道我应该称它们为物体。
然后检查“B”键是否被按下。如果是,那么我们把背景(bg)设置为从关到开,或是从开到关。
环境贴图是以这种方法完成的。我们检查”E”键是否被按下,如果是我们把env的值从TRUE设置为FALSE,或是从FALSE设置为TRUE。把环境贴图打开或是关闭!
每次调用Update()的时候angle都会增加一个很小的值。我把时间除以60来使得它运行慢一点。
void Update (DWORD milliseconds) // Perform Motion Updates Here
{
if (g_keys->keyDown [VK_ESCAPE] == TRUE) // Is ESC Being Pressed?
{
TerminateApplication (g_window); // Terminate The Program
}
if (g_keys->keyDown [VK_F1] == TRUE) // Is F1 Being Pressed?
{
ToggleFullscreen (g_window); // Toggle Fullscreen Mode
}
if ((g_keys->keyDown [' ']) && !sp) // Is Space Being Pressed And Not Held?
{
sp=TRUE; // Set sp To True
effect++; // Change Effects (Increase effect)
if (effect>3) // Over Our Limit?
effect=0; // Reset Back To 0
}
if (!g_keys->keyDown[' ']) // Is Space Released?
sp=FALSE; // Set sp To False
if ((g_keys->keyDown ['B']) && !bp) // Is 'B' Being Pressed And Not Held?
{
bp=TRUE; // Set bp To True
bg=!bg; // Toggle Background Off/On
}
if (!g_keys->keyDown['B']) // Is 'B' Released?
bp=FALSE; // Set bp To False
if ((g_keys->keyDown ['E']) && !ep) // Is 'E' Being Pressed And Not Held?
{
ep=TRUE; // Set ep To True
env=!env; // Toggle Environment Mapping Off/On
}
if (!g_keys->keyDown['E']) // Is 'E' Released?
ep=FALSE; // Set ep To False
angle += (float)(milliseconds) / 60.0f; // Update angle Based On The Timer
在最初的教程中,所有的AVI文件都以相同的速度播放。然后教程被用正确的播放速度重新写了一遍,自从这部分代码被调用的时候,next的值就被加上经过了的毫秒数。如果你记得我们在教程的前面的部分,我们计算了每帧显示的毫秒数(mpf),为了计算当前的帧,我们获得了经过的时间(next)并用每帧显示的时间(mpf)来除以它。
在那之后,我们检查并确定当前的动画帧有没有超过视频的上一个帧。如果是的,那么frame的值被设置为0,动画重新开始。
下面这段代码会在你的电脑运行太慢或其它程序占用CPU的时候的时候丢掉一些帧,如果你想要显示所有的帧而不管使用者的电脑速度有多慢的话,你应该检查next的值是否大于mpf。如果是,那么你就把next的值设为0并且把frame的值增加1。两种方法都能工作。下面这段代码对较快的电脑运行效果比较好。
如果你精力充沛的话,那么试着把重新播放、快进、暂停、和向后播放!
next+= milliseconds; // Increase next Based On Timer (Milliseconds)
frame=next/mpf; // Calculate The Current Frame
if (frame>=lastframe) // Have We Gone Past The Last Frame?
{
frame=0; // Reset The Frame Back To Zero (Start Of Video)
next=0; // Reset The Animation Timer (next)
}
}
现在的是绘制代码,我们清空屏幕和深度缓存。然后我们从动画中抓取了一帧。重申一遍,我只是努力让它简单!你传递了一个需要的帧(frame)给GrabAVIFrame()。相当简单!当然如果你想要多个AVI帧,你就应该传递一个纹理标志。(更多的等着你来完成)。
void Draw (void) // Draw Our Scene
{
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Clear Screen And Depth Buffer
GrabAVIFrame(frame); // Grab A Frame From The AVI
下面这段代码检查看我们是否要绘制背景图片。如果bg的值为真,我们重置了矩阵为模型模式并且绘制了一个大得能够填满整个屏幕的贴图(贴的是来自AVI文件的视频帧)的长方形。这个长方形在屏幕后20个单位处绘制,使得它看起来就像是在物体的后面(在远处)。
if (bg) // Is Background Visible?
{
glLoadIdentity(); // Reset The Modelview Matrix
glBegin(GL_QUADS); // Begin Drawing The Background (One Quad)
// Front Face
glTexCoord2f(1.0f, 1.0f); glVertex3f( 11.0f, 8.3f, -20.0f);
glTexCoord2f(0.0f, 1.0f); glVertex3f(-11.0f, 8.3f, -20.0f);
glTexCoord2f(0.0f, 0.0f); glVertex3f(-11.0f, -8.3f, -20.0f);
glTexCoord2f(1.0f, 0.0f); glVertex3f( 11.0f, -8.3f, -20.0f);
glEnd(); // Done Drawing The Background
}
在绘制了(或没绘制)背景之后,我们重置矩阵为模型模式(让我们又回到了屏幕的中心).然后我们向屏幕内移动了10个单位。
在那之后,我们检查看env的值是否为真。如果是,我们打开球的贴图来创建环境贴图效果。
glLoadIdentity (); // Reset The Modelview Matrix
glTranslatef (0.0f, 0.0f, -10.0f); // Translate 10 Units Into The Screen
if (env) // Is Environment Mapping On?
{
glEnable(GL_TEXTURE_GEN_S); // Enable Texture Coord Generation For S (NEW)
glEnable(GL_TEXTURE_GEN_T); // Enable Texture Coord Generation For T (NEW)
}
我在最后几分钟加入了下面这些代码。我们的物体围绕x轴和y轴旋转(基于angle的值)然后向z轴方向移动了2个单位。这是我们离开了屏幕中心。如果你将这几行移去,物体将围绕屏幕中心旋转。有了这3行代码后,物体就会在自转的同时四处移动。
如果你不明白旋转和移动…你不应该读这篇教程。
glRotatef(angle*2.3f,1.0f,0.0f,0.0f); // Throw In Some Rotations To Move Things Around A Bit
glRotatef(angle*1.8f,0.0f,1.0f,0.0f); // Throw In Some Rotations To Move Things Around A Bit
glTranslatef(0.0f,0.0f,2.0f); // After Rotating Translate To New Position
下面这段代码检查了我们要绘制哪种效果。如果effect的值是0,我们先旋转然后绘制了一个方块。这个旋转使方块在X、Y、Z轴上旋转。到现在为止,在你头脑里应该有创建一个燃烧的方块的代码了吧!
switch (effect) // Which Effect?
{
case 0: // Effect 0 - Cube
glRotatef (angle*1.3f, 1.0f, 0.0f, 0.0f); // Rotate On The X-Axis By angle
glRotatef (angle*1.1f, 0.0f, 1.0f, 0.0f); // Rotate On The Y-Axis By angle
glRotatef (angle*1.2f, 0.0f, 0.0f, 1.0f); // Rotate On The Z-Axis By angle
glBegin(GL_QUADS); // Begin Drawing A Cube
// Front Face
glNormal3f( 0.0f, 0.0f, 0.5f);
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);
// Back Face
glNormal3f( 0.0f, 0.0f,-0.5f);
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);
// Top Face
glNormal3f( 0.0f, 0.5f, 0.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);
// Bottom Face
glNormal3f( 0.0f,-0.5f, 0.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);
// Right Face
glNormal3f( 0.5f, 0.0f, 0.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);
// Left Face
glNormal3f(-0.5f, 0.0f, 0.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(); // Done Drawing Our Cube
break; // Done Effect 0
这就是我们绘制球的地方。我们先在X、Y、Z轴上做了一些旋转,然后绘制了这个球。球的半径是1.3f,有20个分片和20个块。我决定使用20的原因是我并不想让球完全的光滑。使用少一点的片和块是得球体看起来有点粗糙(不光滑),使得它在贴图后还能看出来是在旋转。试着改变这个值!但有一点很重要更多的片和块需要更多的处理能力!
case 1: // Effect 1 - Sphere
glRotatef (angle*1.3f, 1.0f, 0.0f, 0.0f); // Rotate On The X-Axis By angle
glRotatef (angle*1.1f, 0.0f, 1.0f, 0.0f); // Rotate On The Y-Axis By angle
glRotatef (angle*1.2f, 0.0f, 0.0f, 1.0f); // Rotate On The Z-Axis By angle
gluSphere(quadratic,1.3f,20,20); // Draw A Sphere
break; // Done Drawing Sphere
这是我们绘制圆柱的地方。我们先在X、Y、Z轴上做了一些旋转。我们的圆柱的底部和顶部的值分别是1.0f和3.0f个单位高,它由32个片和块组成。如果你减少片和块,圆柱将有更少的多边形组成,看起来就不是很圆了。
在我们绘制圆柱之前,我们沿z轴移动了-1.5f个单位。这样做了以后,我们的圆柱会围绕着中心点旋转。通常的把圆柱放在中心的规则是把它的高度除以2,并在Z轴上沿着相反的方向移动。如果你还没有明白了我所说的,把tranlatef(...)这行代码移走。圆柱就会沿着它的基部旋转,而不是它的中心点。
case 2: // Effect 2 - Cylinder
glRotatef (angle*1.3f, 1.0f, 0.0f, 0.0f); // Rotate On The X-Axis By angle
glRotatef (angle*1.1f, 0.0f, 1.0f, 0.0f); // Rotate On The Y-Axis By angle
glRotatef (angle*1.2f, 0.0f, 0.0f, 1.0f); // Rotate On The Z-Axis By angle
glTranslatef(0.0f,0.0f,-1.5f); // Center The Cylinder
gluCylinder(quadratic,1.0f,1.0f,3.0f,32,32); // Draw A Cylinder
break; // Done Drawing Cylinder
}
下面我们检查env是否为真。如果是的,我们关闭球的贴图。我们使用glFlush()函数来清除绘制管道(确信所有的都在我们绘制下一帧之前已经绘制完成)。
if (env) // Environment Mapping Enabled?
{
glDisable(GL_TEXTURE_GEN_S); // Disable Texture Coord Generation For S (NEW)
glDisable(GL_TEXTURE_GEN_T); // Disable Texture Coord Generation For T (NEW)
}
glFlush (); // Flush The GL Rendering Pipeline
}
我希望你能喜欢这篇教程。现在已经是早上2点了…我已经写了6个小时了。听起来很疯狂,但是写出真正有意义的东西不是意见容易的事。我已经把这篇教程阅读了3遍了,到现在我还是努力让所有的东西更容易明白。不管你信不信,让你明白代码是怎样工作和为什么工作的对我来说是很重要的。这就是为什么我总是喋喋不休的做过多的评论的原因。
|
|