NeHe OpenGL第三十五课:播放AVI
NeHe OpenGL第三十五课:播放AVI
在OpenGL中播放AVI:
在OpenGL中如何播放AVI呢?利用Windows的API把每一帧作为纹理绑定到OpenGL中,虽然很慢,但它的效果不错。你可以试试。
首先我得说我非常喜欢这一章节.Jonathan de Blok使我产生了用OpenGL编写AVI播放器的想法,可那时,我跟本不知如何打开AVI文件,更不必说去写一个播放器了.于是我浏览了搜藏的编程书籍,没有一本讲到AVI
文件的.我又阅读了MSDN上和AVI文件格式有关的一切内容,上面有很多有用的信息,但我需要更多的.
花了几小时在网上搜到AVI范例,只找到两个网站.我的搜索技巧不能说很棒吧,但99.9%的情况,我能找到我要寻找的东西.了解到AVI范例竟如此之少时,我完全震惊了.大多数范例并不能编译通过...有一些则用了太复
杂的的方法(至少对我如此),剩下的不错,可是用VB,Delphi等写的(不是用vc++).
找到的第一个网页是Jonathan Nix写的题为"AVI 文件"的文章.网址是http://www.gamedev.net/reference/programming/features/avifile.感谢Jonathan写了这片关于AVI格式的好文章.虽然我用不同的做法,但他的代码片断和清晰的注解让人学得很轻松!第二个网站标题为"AVI 总体观"(John F. McGowan, Ph.D写的)..我可以大肆赞美John的网叶有多么惊奇,但你最好自己去看看.他的网址是
http://www.jmcgowan.com/avi.html.这个网站讲到了和AVI格式有关的几乎所有内容.感谢John做了一个这么有用的网站.
最后要提到是我没有借鉴任何代码,没有抄袭任何代码.我的代码是花了三天时间了解到上述网站和文章的信息后才写成的.我是想说我的代码也许不是播放AVI文件的最好代码,他也许不是放AVI文件的正确代码,但他管
用而且使用方便.如果你不喜欢这些代码和我的编程风格,或者觉得我的言论伤害到整个编程界,你有以下选择:1)在网上找到替换的资源2)写自己的AVI播放器3)写一篇更好的文章.任何访问本网站的人现在应该知道我
只是一名中级程序员(这一点我在网站里很多文章的开头都提到过)!我编写代码自乐而已.本网站的目的在于让非精英程序员更轻松的开始OpenGl编程.这些文章只是关于我实现的几个特殊的效果...没有其他的.
开始讲代码首先你要注意的是我们要包括和连接到视频头文件和库文件.非常感谢微软(窝不敢相信我说了什么).库文件使打开,播放AVI文件都很简便.现在你要知道的是必须包括头文件vfw.h而且要连接到vfw32.lib
库文件如果想编译你的代码的话:)
#include <vfw.h> // Video For Windows头文件
#include "NeHeGL.h" // NeHeGL头文件
#pragma comment( lib, "opengl32.lib" )
#pragma comment( lib, "glu32.lib" )
#pragma comment( lib, "vfw32.lib" ) // 链接到VFW32.lib
GL_Window* g_window;
Keys* g_keys;
现在定义变量.angle是用来根据时间来旋转物体的.为简单起见我们用angle来控制所有的旋转.
接下来是一个整型变量是用来计算经过的时间(以毫秒计).它使帧速保持一个速度.
后面细讲!
frame是动画要显示的当前帧,初始值为0(第一帧).我想如果成功打开AVI,他至少有一帧吧,这样假定比较安全:)
effect是当前屏幕上的效果(有:立方体,球体,圆柱体).env是布尔值.若它为true则环境映射启动,若为假,则物体没有环境映射.若bg为true,你会看到物体后有全屏的动画;若为假,你只会看到物体(没有背景).
sp,ep和bp用来确定使用者没有按着键不放.
float angle; // 旋转用
int next; // 动画用
int frame=0; // 帧计数器
int effect; // 当前效果
bool sp; // 空格键按下?
bool env=TRUE; // 环境映射(默认开)
bool ep; //’E’ 按下?
bool bg=TRUE; // 背景(默认开)
bool bp; // ’B’ 按下?
psi结构体包含AVI文件信息.pavi缓冲的指针,缓冲用来接受AVI文件打开时的流句柄.pgf是指向GetFrame对象的指针.bmih在后面的代码中将被用来把动画的每一帧转换为我们需要的格式(保存位图的头信
息).lastframe保存AVI动画最后一帧的序号.width和height保存AVI流的维信息,最后...pdata是图象数据的指针(每次在从AVI中获得一帧后返回).mpf用来计算每帧需要多少毫秒.后面细谈这个变量.
AVISTREAMINFO psi; // 包含流信息的结构体的指针
PAVISTREAM pavi; // 流句柄
PGETFRAME pgf; // GetFrame对象的指针
BITMAPINFOHEADER bmih; // 头信息 For DrawDibDraw
long lastframe; // 流中最后一帧
int width; // 视频宽
int height; // 视频高
char *pdata; // 纹理数据指针
int mpf; // 控制每帧显示时间
在本章中我们用GLU库创建两个二次曲面(球体和圆柱体).quadratic是曲面对象的指针.
hdd是DrawDib设备上下文的句柄.hdc是设备上下文的句柄.
hBitmap是设备无关位图的句柄(在后面位图转换时用到).
data是最后指向转换后位图的图象数据的指针,在后面的代码中会有意义,往下读:)
GLUquadricObj *quadratic; // 存储二次曲面对象
HDRAWDIB hdd; // Dib句柄
HBITMAP hBitmap; // 设备无关位图的句柄
HDC hdc = CreateCompatibleDC(0); // 创建一个兼容的设备上下文
unsigned char* data = 0; // 调整后的图象数据指针
下面使用到汇编语言.那些从来没有用过汇编的不要被吓倒了.他看起来神秘,实际上非常简单!
在写本章是我发现了十分奇怪的事.第一次做出来的可以播放,但色彩混乱了.本来是红色的变成蓝色的了,本来是蓝色的变成红色的了.我简直要发狂了!我相信我的代码某处有问题.看了一边代码还是找不到bug于是又读
了MSDN.为什么红色与蓝色互换了!?!MSDN明明说24比特位图是RGB啊!又读了一些东西,我找到了答案.在WINDOWS图形系统中,RGB数据是倒着存储的(BGR).而在OpenGL中,要用的RGB数据就是RGB的顺序!
在抱怨了微软之后:)我决定加一条注解!我不因为RGB数据倒过来存放而打算骂微软.只是觉得很奇怪--他叫做RGB实际上在文件中是按BGR存的!
另:这一点和"little endian"和"big endian"有关.Intel以及Intel兼容产品用little endian--LSB(数据最低位)首先存.OpenGL是产生于Silicon Graphics的机器的,用的是big endian,所以标准
的OpenGL要位图格式是big endian格式.这是我的理解.
棒极了!所以说这第一个播放器就是一个垃圾!我的解决方法是用一个循环把数据交换过来.这能行,但太慢.我又在纹理生成代码中用GL_BGR_EXT代替了GL_RGB,速度暴升,色彩显示也对了!问题解决了...原来我是这样
想!后来发现一些OpenGL驱动不支持GL_BGR... :(
与好友Maxwell Sayles讨论后,他推荐我用汇编代码来交换数据.一分钟后,他用icq发来下面的代码!也许不是最优化的,但他很快也很有效!
动画的每一帧存在一个缓冲里.图象256像素宽,256像素高,每个色彩一字节(一像素3字节).下面的代码会扫描整个缓冲并交换红与蓝的字节.红存在ebx+0,蓝存在ebx+2.我们一次向前走3字节(因为一个像素3字节).
不断扫描直到所有数据交换过来.
你们有些人不喜欢用汇编代码,所以我想有必要在本章里解释一下.本来计划用GL_BGR_EXT,他管用,但不是所有的显卡都支持!我又用异或交换法,这在所有机器上都是有效的,但不十分快.用了汇编后速度相当快.考虑到
我们在处理实时视频,你需要最快的交换方法.权衡了以上选择,汇编是最好的!如果你有更好的办法,就用你自己的吧!我并不是告诉你必须如何去做,只是告诉你我的做法.我也会细致的解释代码.如果你要用更好的代码来
作替换,你要清楚这些代码是来干什么的,自己写代码时,要为日后的优化提供方便.
void flipIt(void* buffer) // 交换红蓝数据(256x256)
{
void* b = buffer; // 缓冲指针
__asm // 汇编代码
{
mov ecx, 256*256 // 设置计数器
mov ebx, b // ebx存数据指针
label: // 循环标记
mov al,[ebx+0] // 把ebx位置的值赋予al
mov ah,[ebx+2] // 把ebx+2位置的值赋予ah
mov [ebx+2],al // 把al的值存到ebx+2的位置
mov [ebx+0],ah // 把ah的值存到ebx+0的位置
add ebx,3 // 向前走3个字节
dec ecx // 循环计数器减1
jnz label // ecx非0则跳至label
}
}
下面的代码以只读方式打开AVI文件.szFile是打开文件的名字.title[100]用来修改window标题(显示AVI文件信息).
首先调用AVIFileInit().他初始化AVI文件库(使东西能用?鹄?).
打开AVI文件有很多方法.我采用AVIStreamOpenFromFile(...).他能打开AVI文件中单独一个流(AVI文件可以包含多个流).它的参数如下:pavi是接收流句柄的缓冲的指针,szFile是打开文件的名字(包括路
径).第三参数是打开的流的类型.在这个工程里,我们只对视频流感兴趣(streamtypeVIDEO).第四参数是0,这表示我们需要第一次读到的视频流(一个AVI文件里会有多个视频流,我们要第一个).OF_READ表示以只读
方式打开文件.最后一个参数是一个类标识句柄的指针.说实话,我也不清楚他是干吗的.我让windows自己设定,于是把NULL传过去.
void OpenAVI(LPCSTR szFile) // 打开AVI文件szFile
{
TCHAR title[100]; // 包含修改了的window标题
AVIFileInit(); // 打开AVI文件库
// 打开AVI流
if (AVIStreamOpenFromFile(&pavi, szFile, streamtypeVIDEO, 0, OF_READ, NULL) !=0)
{
// 打开流时的出错处理
MessageBox (HWND_DESKTOP, "打开AVI流失败", "错误", MB_OK | MB_ICONEXCLAMATION);
}
到目前为止,我们假定文件被正确打开,流被正确定位!然后用AVIStreamInfo(...)从AVI文件里抓取一些信息.
先前我们创建了叫psi的结构体来保存AVI流的信息.下面第一行,我们把AVI信息填入该结构体.从流的宽度(以像素计)到动画的帧速等所有的信息都会存到psi中.那些想要精确控制播放速度的要记住我刚才说的.更多
的信息参阅MSDN.
我们通过右边位置减左边位置算出帧宽.这个结果是以像素记的精确的帧宽.至于高度,可以用底边位置减顶边位置得到.这样得到高度的像素值.
然后用AVIStreamLength(...)得到AVI文件最后一帧的序号.AVIStreamLength(...)返回动画最后一帧的序号.结果存在lastframe里.
计算帧速很简单.每秒帧速(fps)= psi.dwRate/psi,dwScale.返回的值应该匹配显示帧的速度(你在AVI动画中右击鼠标可以看到).你会问那么这和mpf有什么关系呢?第一次写这个代码时,我试着用fps来选择动
画了正确的帧面.我遇到一个问题...视频放的太快!于是我看了一下视频属性.face2.avi文件有3.36秒长.帧速是29.974fps.视频动画共有91帧.而3.36*29.974 = 100.71.非常奇怪!!
所以我采用一些不同的方法.不是计算帧速,我计算每一帧播放所需时间.AVIStreamSampleToTime()把在动画中的位置转换位你到达该位置所需的时间(毫秒计).所以通过计算到达最后一帧的时间就得到整个动画的
播放时间.再拿这个结果除以动画总帧数(lastframe).这样就给出了每帧的显示时间(毫秒计).结果存在mpf(milliseconds per frame)里.你也能通过获取动画中一帧的时间来算每帧的毫秒数,代码
为:AVIStreamSampleToTime(pavi,1).两种方法都不错!非常感谢Albert Chaulk提供思路!
我说每帧的毫秒数不精确是因为mpf是一个整型值,所以所有的浮点数都会被取整.
AVIStreamInfo(pavi, &psi, sizeof(psi)); // 把流信息读进psi
width=psi.rcFrame.right-psi.rcFrame.left; // 宽度为右边减左边
height=psi.rcFrame.bottom-psi.rcFrame.top; // 高为底边减顶边
lastframe=AVIStreamLength(pavi); // 最后一帧的序号
mpf=AVIStreamSampleToTime(pavi,lastframe)/lastframe; // mpf的不精确值
因为OpenGL需要纹理数据是2的幂,而大多视频是160*120,320*240等等,所以需要一种把视频格式重调整为能用作纹理的格式.我们可利用Windows Dib函数去做.
首先要做的是描述我们想要的图像的类型.于是我们要以所需参数填好bmih这个BitmapInfoHeader结构.
首先设定该结构体的大小.再把位平面数设为1.3字节的数据有24比特(RGB).要使图像位256像素宽,256像素高,最后要让数据返回为UNCOMPRESSED(非压缩)的RGB数据(BI_RGB).
CreateDIBSection创建一个可直接写的设备无关位图(dib).如果一切顺利,hBitmap会指向该dib的比特值.hdc是设备上下文(DC)的句柄第二参数是BitmapInfo结构体的指针.该结构体包含了上述dib文件的信
息.第三参数(DIB_RGB_COLORS)设定数据是RGB值.data是指向DIB比特值位置的指针的指针(呜,真绕口).第五参数设为NULL,我们的DIB已被分配好内存.末了,最后一个参数可忽略(设为NULL).
引自MSDN:SelecObject函数选一个对象进入设备上下文(DC).
现在我们建好一个能直接写的DIB,yeah:)
bmih.biSize = sizeof (BITMAPINFOHEADER); // BitmapInfoHeader的大小
bmih.biPlanes = 1; // 位平面
bmih.biBitCount = 24; //比特格式(24 Bit, 3 Bytes)
bmih.biWidth = 256; // 宽度(256 Pixels)
bmih.biHeight = 256; // 高度 (256 Pixels)
bmih.biCompression = BI_RGB; // 申请的模式 = RGB
hBitmap = CreateDIBSection (hdc, (BITMAPINFO*)(&bmih), DIB_RGB_COLORS, (void**)(&data), NULL, NULL);
SelectObject (hdc, hBitmap); // 选hBitmap进入设备上下文(hdc)
在从AVI中读取帧面前还有几件事要做.接下来使程序做好从AVI文件中解出帧面的准备.用AVIStreamGetFrameOpen(...)函数做这一点.
你能给这个函数传一个结构体作为第二参数(它会返回一个特定的视频格式).糟糕的是,你能改变的唯一数据是返回的图像的宽度和高度.MSDN也提到能传AVIGETFRAMEF_BESTDISPLAYFMT为参数来选择一个最佳显示
格式.奇怪的是,我的编译器没有定义这玩艺儿.
如果一切顺利,一个GETFRAME对象被返回(用来读帧数据).有问题的话,提示框会出现在屏幕上告诉你有错误!
pgf=AVIStreamGetFrameOpen(pavi, NULL); // 用要求的模式建PGETFRAME
if (pgf==NULL)
{
// 解帧出错
MessageBox (HWND_DESKTOP, "不能打开AVI帧", "错误", MB_OK | MB_ICONEXCLAMATION);
}
下面的代码把视频宽,高和帧数传给window标题.用函数SetWindowText(...)在window顶部显示标题.以窗口模式运行程序看看以下代码的作用.
// bt标题栏信息(宽 / 高/ 帧数)
wsprintf (title, "NeHe's AVI Player: Width: %d, Height: %d, Frames: %d", width, height, lastframe);
SetWindowText(g_window->hWnd, title); // 修改标题栏
}
下面是有趣的东西...从AVI中抓取一帧,把它转为大小和色深可用的图象.lpbi包含一帧的BitmapInfoHeader信息.我们在下面第二行完成了几件事.先是抓了动画的一帧...我们需要的帧面由这些帧确定.这会让动
画走掉这一帧,lpbi会指向这一帧的头信息.
下面是有趣的东西...我们要指向图像数据了.要跳过头信息(lpbi->biSize).一件事直到写本文时我才意识到:也要跳过任何的色彩信息.所以要跳过biClrUsed*sizeof(RGBQUAD)(译者:我想他是说要跳过调色
板信息).做完这一切,我们就得到图像数据的指针了(pdata).
也要把动画的每一帧的大小转为纹理能用的大小,还要把数据转为RGB数据.这用到DrawDibDraw(...).
一个大概的解释.我们能直接写设定的DIB图像.那就是DrawDibDraw(...)所做的.第一参数是DrawDib DC的句柄.第二参数是DC的句柄.接下来用左上角(0,0)和右下角(256,256)构成目标矩形.
lpbi指向刚读的帧的bitmapinfoheader信息.pdata是刚读的帧的图像数据指针.
再把源图象(刚读的帧)的左上角设为(0,0),右下角设为(帧宽,帧高).最后的参数应设为0.
这个方法可把任何大小、色深的图像转为256*256*24bit的图像.
void GrabAVIFrame(int frame) // 从流中抓取一帧
{
LPBITMAPINFOHEADER lpbi; // 存位图的头信息
lpbi = (LPBITMAPINFOHEADER)AVIStreamGetFrame(pgf, frame); // 从AVI流中得到数据
pdata=(char *)lpbi+lpbi->biSize+lpbi->biClrUsed * sizeof(RGBQUAD); // 数据指针,由AVIStreamGetFrame返回(跳过头
//信息和色彩信息)
// 把数据转为所需格式
DrawDibDraw (hdd, hdc, 0, 0, 256, 256, lpbi, pdata, 0, 0, width, height, 0);
我们得到动画的每帧数据(红蓝数据颠倒的).为解决这个问题,我们的高速代码flipIt(...).记住,data是指向DIB比特值位置的指针的指针变量.这意味着调用DrawDibDraw后,data指向一个调整过大小
(256*256),修改过色深(24bits)的位图数据.
原来我通过重建动画的每一帧来更新纹理.我收到几封email建议我用glTexSubImage2D().翻阅了OpenGL红宝书后,我磕磕绊绊的写出下面注释:"创建纹理的计算消耗比修改纹理要大.在OpenGL1.1版本中,有几条
调用能更新全部或部分纹理图像信息.这对某些应用程序有用,比如实时的抓取视频图像作纹理.对于这些程序,用glTexSubImage2D()根据新视频图像来创建单个纹理以代替旧的纹理数据是行得通的."
在我个人并没有发现速度明显加快,也许在低端显卡上才会.glTexSubImage2D()的参数是:目标是一个二维纹理(GL_TEXTURE_2D).细节级别(0),mipmapping用.x(0),y(0)告诉OpenGL开始拷贝的位置(0,0是
纹理的左下角).然后是图像的宽度,我们要拷贝的图像是256像素宽,256像素高.GL_RGB是我们的数据格式.我们在拷贝无符号byte.最后...图像数据指针----data.非常简单!
Kevin Rogers 另加:我想指出使用glTexSubImage2D()另一个重要原因.不仅因为在许多OpenGL实现中它很快,还因为目标区不必是2的幂.这对视频重放很方便,因为一帧的维通常不是2的幂(而是像320*200之类
的).这样给了你很大机动性,你可以按视频流原本的样子播放,而不是扭曲或剪切每一帧来适应纹理的维.
重要的是你不能更新一个纹理如果你第一次没有创建他!我们在Initialize()中创建纹理.
还要提到的是...如果你计划在工程里使用多个纹理,务必绑住你要更新的纹理.否则,更新出来的纹理也许不是你想要的!
flipIt(data); // 交换红蓝数据
// 更新纹理
glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, 256, 256, GL_RGB, GL_UNSIGNED_BYTE, data);
}
接下来的部分当程序退出时调用,我们关掉DrawDib DC,释放占用的资源.然后释放AVI GetFrame资源.最后释放AVI流和文件.
void CloseAVI(void) // 关掉AVI资源
{
DeleteObject(hBitmap); //释放设备无关位图信息
DrawDibClose(hdd); // 关掉DrawDib DC
AVIStreamGetFrameClose(pgf); // 释放AVI GetFrame资源
AVIStreamRelease(pavi); // 释放AVI流
AVIFileExit(); // 释放AVI文件
}
初始化很简明.设初始的angle为0.再打开DrawDib库(得到一个DC).一切顺利的话,hdd会是新创建的dc的句柄.
以黑色清屏,开启深度测试,等等.
然后建一个新的二次曲面.quadratic是这个新对象的指针.设置光滑的法线,允许纹理坐标的生成.
BOOL Initialize (GL_Window* window, Keys* keys)
{
g_window = window;
g_keys = keys;
// 开始用户的初始
angle = 0.0f; // angle为0先
hdd = DrawDibOpen(); // 得到Dib的DC
glClearColor (0.0f, 0.0f, 0.0f, 0.5f); // 黑色背景
glClearDepth (1.0f); // 深度缓冲初始
glDepthFunc (GL_LEQUAL); // 深度测试的类型(小于或等于)
glEnable(GL_DEPTH_TEST); // 开启深度测试
glShadeModel (GL_SMOOTH); // 平滑效果
glHint (GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST); // 透视图计算设为 //最高精度
quadratic=gluNewQuadric(); // 建二次曲面的指针
gluQuadricNormals(quadratic, GLU_SMOOTH); // 设置光滑的法线
gluQuadricTexture(quadratic, GL_TRUE); // 创建纹理坐标
下面的代码中,我们开启2D纹理映射,纹理滤镜设为GLNEAREST(最快,但看起来很糙),建立球面映射(为了实现环境映射效果).试试其它滤镜,如果你有条件,可以试试GLLINEAR得到一个平滑的动画效果.
设完纹理和球面映射,我们打开.AVI文件.我尽量使事情简单化...你能看出来么:)我们要打开的文件叫作facec2.avi
glEnable(GL_TEXTURE_2D); // 开启2D纹理映射
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST);// 设置纹理滤镜
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); // 设纹理坐标生成模式为s
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP); // 设纹理坐标生成模式为t
OpenAVI("data/face2.avi"); // 打开AVI文件
// 创建纹理
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 256, 256, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
return TRUE; // 初始化成功返回TRUE
}
关闭时调用CloseAVI().他正确的关闭AVI文件,并释放所有占用资源.
void Deinitialize (void) // 做所有的释放工作
{
CloseAVI(); // 关闭AVI文件
}
到了检查按键和更新旋转角度的地方了.我知道再没有必要详细解释这些代码了.我们检查空格键是否按下,若是,则增加effect值.有3种效果(立方,球,圆柱)第四个效果被选时(effect = 3)不画任何对象...仅显示
背景!如果选了第四效果,空格又按下了,就重设为第一个效果(effect = 0).Yeah,我本该叫他对象:)
然后检查’b’键是否按下,若是,则改变背景(bg从ON到OFF或从OFF到ON).
环境映射的键设置也一样.检查’E’是否按下,若是则改变env从TRUE到FALSE或从FALSE到TRUE.仅仅是关闭或开启环境映射!
每次调用Updata()时angle都加上一个小分数.我用经过的时间除以60.0f使速度降一点.
void Update (DWORD milliseconds) // 动画更新
{
if (g_keys->keyDown [VK_ESCAPE] == TRUE) //ESC按下?
{
TerminateApplication (g_window); // 关闭程序
}
if (g_keys->keyDown [VK_F1] == TRUE) // F1按下?
{
ToggleFullscreen (g_window); // 改变显示模式
}
if ((g_keys->keyDown [' ']) && !sp) // 空格按下并已松开
{
sp=TRUE; // 设sp为True
effect++; // 增加effect
if (effect>3) // 超出界限?
effect=0; // 重设为0
}
if (!g_keys->keyDown[' ']) // 空格没按下?
sp=FALSE; // 设sp为False
if ((g_keys->keyDown ['B']) && !bp) // ’B’按下并已松开
{
bp=TRUE; // 设bp为True
bg=!bg; // 改变背景 Off/On
}
if (!g_keys->keyDown['B']) // ’B’没按下?
bp=FALSE; // 设bp为False
if ((g_keys->keyDown ['E']) && !ep) // ’E’按下并已松开
{
ep=TRUE; // 设ep为True
env=!env; // 改变环境映射 Off/On
}
if (!g_keys->keyDown['E']) //’E’没按下
ep=FALSE; // 设ep为False
angle += (float)(milliseconds) / 60.0f; // 根据时间更新angle
在原来的文章里,所有的AVI文件都以相同的速度播放.于是,我重写了本文让视频以正常的速度播放.next增加经过的毫秒数.如果你记得文章的前面,我们算出了显示每帧的毫秒数(mpf).为了计算当前帧,我们拿经过的
时间除以显示每帧的毫秒数(mpf).
还要检查确定当前帧没有超过视频的最后帧.若超过了,则将frame设为0,动画计时器设为0,于是动画从头开始.
下面的代码会丢掉一些帧,若果你的计算机太慢的话,
或者另一个程序占用了CPU.如果想显示每一帧而不管计算机有多慢的话,你要检查next是否比mpf大,若是,你要把next设为0,frame增1.两种方法都行,虽然下面的代码更有利于跑的快的机器.
如果你有干劲,试着加上循环,快速播放,暂停或倒放等功能.
next+= milliseconds; // 根据时间增加next
frame=next/mpf; // 计算当前帧号
if (frame>=lastframe) // 超过最后一帧?
{
frame=0; // Frame设为0
next=0; // 重设动画计时器
}
}
下面是画屏代码:)我们清屏和深度缓冲.再抓取动画的一帧.我将使这更简单!把你想要的帧数传给GrabAVIFrame().非常简单!当然,如果是多个AVI,你要传一个纹理标号.(你要做更多的事)
void Draw (void) // 绘制我们的屏幕
{
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // 清屏和深度缓冲
GrabAVIFrame(frame); // 抓取动画的一帧
下面检查我们是否想画一个背景图.若bg是TRUE,重设模型视角矩阵,画一个单纹理映射的能盖住整个屏幕的矩形(纹理是从AVI从得到的一帧).矩形距离屏面向里20个单位,这样它看起来在对象之后(距离更远).
if (bg) // 背景可见?
{
glLoadIdentity(); // 重设模型视角矩阵
glBegin(GL_QUADS); // 开始画背景(一个矩形)
// 正面
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();
}
画完背景(或没有),重设模型视角矩阵(使视角中心回到屏幕中央).视角中心再向屏内移进10个单位.然后检查env是否为TRUE.若是,开启球面映射来实现环境映射效果.
glLoadIdentity (); // 重设模型视角矩阵
glTranslatef (0.0f, 0.0f, -10.0f); // 视角中心再向屏内移进10个单位
if (env) // 环境映射开启?
{
glEnable(GL_TEXTURE_GEN_S); // 开启纹理坐标生成S坐标
glEnable(GL_TEXTURE_GEN_T); // 开启纹理坐标生成T坐标
}
在最后关头我加了以下代码.他绕X轴和Y轴旋转(根据angle的值)然后在Z轴方向移动2单位.这使我们离开了屏幕中心.如果删掉下面三行,对象会在屏幕中心打转.有了下面三行,对象旋转时看起来离我们远一些:)
如果你不懂旋转和平移...你就不该读这一章:)
glRotatef(angle*2.3f,1.0f,0.0f,0.0f); // 加旋转让东西动起来
glRotatef(angle*1.8f,0.0f,1.0f,0.0f); // 加旋转让东西动起来
glTranslatef(0.0f,0.0f,2.0f); // 旋转后平移到新位置
下面的代码检查我们要画哪一个对象.若effect为0,我们做一些旋转在画一个立方体.这个旋转使立方体绕X,Y,Z轴旋转.现在你脑中该烙下建一个立方体的方法了吧:)
switch (effect) // 哪个效果?
{
case 0: // 效果 0 - 立方体
glRotatef (angle*1.3f, 1.0f, 0.0f, 0.0f);
glRotatef (angle*1.1f, 0.0f, 1.0f, 0.0f);
glRotatef (angle*1.2f, 0.0f, 0.0f, 1.0f);
glBegin(GL_QUADS);
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);
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);
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);
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);
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);
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();
break;
下面是画球体的地方.开始先绕X,Y,Z轴旋转,再画球体.球体半径为1.3f,20经线,20纬线.我用20是因为我没打算让球体非常光滑.少用些经纬数,使球看起来不那么光滑,这样球转起来时就能看到球面映射的效果(当然
球面映射必须开启).试着尝试其它值!要知道,使用更多的经纬数需要更强的计算能力!
case 1: // 效果1,球体
glRotatef (angle*1.3f, 1.0f, 0.0f, 0.0f);
glRotatef (angle*1.1f, 0.0f, 1.0f, 0.0f);
glRotatef (angle*1.2f, 0.0f, 0.0f, 1.0f);
gluSphere(quadratic,1.3f,20,20);
break;
下面画圆柱.开始先绕X,Y,Z轴旋转,圆柱顶和底的半径都为1.0f.高3.0f,32经线,32纬线.若减少经纬数,圆柱的组成多边形会减少,他看起来就没那么圆.
case 2: // 效果2,圆柱
glRotatef (angle*1.3f, 1.0f, 0.0f, 0.0f);
glRotatef (angle*1.1f, 0.0f, 1.0f, 0.0f);
glRotatef (angle*1.2f, 0.0f, 0.0f, 1.0f);
glTranslatef(0.0f,0.0f,-1.5f);
gluCylinder(quadratic,1.0f,1.0f,3.0f,32,32);
break;
}
下面检查env是否为TRUE,若是,关闭球面映射.调用glFlush()清空渲染流水线(使在下一帧开始前一切都渲染了).
if (env) // 是否开启了环境渲染
{
glDisable(GL_TEXTURE_GEN_S); // 关闭纹理坐标S
glDisable(GL_TEXTURE_GEN_T); // 关闭纹理坐标T
}
glFlush (); // 清空渲染流水线
}
原文及其个版本源代码下载:
http://nehe.gamedev.net/data/lessons/lesson.asp?lesson=35
转载于:https://blog.51cto.com/yarin/381889
相关文章:
为什么TCP的TIME_WAIT状态要保持2MSL?
TIMEWAIT状态也称为 2MSL等待状态。每个具体TCP实现必须选择一个报文段最大生存时间MSL(Maximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。我们知道这个时间是有限的,因为TCP报文段以IP数据报在网络内传输,而IP数据报则有限制其生…
深度 | 一文读懂“情感计算”在零售中的应用发展
作者 | 黄程韦博士、刘刚、包飞博士、杨现博士、孙皓博士、沈艺博士来源 | 苏宁零售技术研究院零售商需要不断通过创新服务来提高顾客的购物体验,而情感计算在该领域具有独特优势。它在零售行业的应用,主要集中在提升购物体验的服务中。在这个科技逐步改…

mysql基于replication实现最简单的M-S主从复制
2019独角兽企业重金招聘Python工程师标准>>> 什么是replication Replication可以实现数据从一台数据库服务器(master)复制到一到多台数据库服务器。 默认情况下,属于异步复制,因此无需维持长连接。 通过配置࿰…

Linux下高并发socket最大连接数所受的各种限制
修改最大打开文件数 # ulimit -n 修改最大进程数 # ulimit -u ------------------------------------------------------ Linux下高并发socket最大连接数所受的各种限制 转自:http://blog.csdn.net/guowake/article/details/6615728 1、修改用户进程可打开…

linux安全问答(1)
一、如何限制对系统资源的过度使用? (1)、编辑/etc/security/limits.conf文件,在其中加入或改变下面这些内容: * hard core 0 //禁止创建core文件 * hard rss 5000 //表示除root用户之外,其他用户都只能最多…
快速搭建对话机器人,就用这一招!
作者 | Milvus.io 责编 | 胡巍巍问答系统是自然语言处理领域一个很经典的问题,它用于回答人们以自然语言形式提出的问题,有着广泛的应用。其经典应用场景包括:智能语音交互、在线客服、知识获取、情感类聊天等。常见的分类有:生成…

目前流行的源程序版本管理软件和项目管理软件都有哪些?各有什么优缺点?...
目前流行的源程序版本管理软件和项目管理软件:Microsoft TFS,Github,SVN,Coding 各自的优缺点: Microsoft TFS:优点:任务版上能将需求、项目进度一览无余,对于小团队而言,…

孙鑫mfc学习笔记第十四课
第十四课网络的相关知识,网络程序的编写,Socket是连接应用程序与网络驱动程序的桥梁,Socket在应用程序中创建,通过bind与驱动程序建立关系。此后,应用程序送给Socket的数据,由Socket交给驱动程序向网络上发…
Linux环境编译安装Mysql以及补装innodb引擎方法
mysql安装 5.6以后可能会收费,所以选择5.1以下从台湾中山大学镜像下载 1.首先要安装C编译环境 # yum install gcc-c 2.下载解压 # wget http://mysql.cdpa.nsysu.edu.tw/Downloads/MySQL-5.1/mysql-5.1.73.tar.gz# tar zxvf mysql-5.1.73.tar.gz# cd mysql-5…
Python 炫技操作:合并字典的七种方法
来源 | Python编程时光(ID: Cool-Python)Python 语言里有许多(而且是越来越多)的高级特性,是 Python 发烧友们非常喜欢的。在这些人的眼里,能够写出那些一般开发者看不懂的高级特性,就是高手&am…

shell脚本编程基础(1)及RAID阵列
shell脚本:Linux从底层到上层的系统架构:硬件-->内核-->库(lib)-->shell-->用户。shell既是一种命令语言,也是程序设计语言(shell脚本),作为一种命令语言,它提供了用户与内核的交互…

freemarker基本语法及实例
EG.一个对象BOOK 1.输出 ${book.name} 空值判断:${book.name?if_exists }, ${book.name?default(‘xxx’)}//默认值xxx ${ book.name!"xxx"}//默认值xxx 日期格式:${book.date?string(yyyy-MM-dd)} 数字格式:${boo…
前百度主任架构师创业,两年融资千万美元,他说AI新药研发将迎来黄金十年...
「AI技术生态论」 人物访谈栏目是CSDN发起的百万人学AI倡议下的重要组成部分。通过对AI生态专家、创业者、行业KOL的访谈,反映其对于行业的思考、未来趋势的判断、技术的实践,以及成长的经历。2020年,CSDN将对1000人物进行访谈,形…
Linux环境安装卸载JDK以及安装Tomcat和发布Java的web程序
Linux环境:CentOS7.2 一.安装JDK 安装好的CentOS会自带OpenJdk,最好还是先卸载系统自带的JDK,然后自己重新去Oracle网站下载最新的JDK安装。 1.卸载系统自带的JDK 查看java信息 # java -version 查看JDK # rpm -qa | grep java 或者 还…
(转)详解css3弹性盒模型(Flexbox)
今天刚学了css3的弹性盒模型,这是一个可以让你告别浮动、完美实现垂直水平居中的新特性。 Flexbox是布局模块,而不是一个简单的属性,它包含父元素和子元素的属性。 Flexbox布局的主体思想是似的元素可以改变大小以适应可用空间,当…
Java开发环境的搭建以及使用eclipse创建项目
一、Java 开发环境的搭建 这里主要说windows环境下怎么配置Java环境。如果是Linux环境参考本博客另一篇文章即可: Linux环境安装卸载JDK 1.首先安装JDK java的SDK简称JDK。 去官网下载最新的JDK即可: http://www.oracle.com/technetwork/java/javase…
MMIT冠军方案 | 用于行为识别的时间交错网络,商汤公开视频理解代码库
作者 | 商汤出品 | AI科技大本营(ID:rgznai100)本文主要介绍三个部分:一个高效的SOTA视频特征提取网络TIN,发表于AAAI2020ICCV19 MMIT多标签视频理解竞赛冠军方案,基于TIN和SlowFast一个基于PyTorch,包含大…

MySQL的主从服务器配置
MySQL的主从服务器配置常见开源数据库有:MySQL,PostgreSQL,SQLite等,商业性质的:Oracle,Sql Server,DB2,Sybase,Infomix其中,Oracle的版本有Oracle 11g,Oracl…

Anaconda中安装Orange3脚本-完整版
2019独角兽企业重金招聘Python工程师标准>>> #Anaconda中安装Orange3脚本,完整版。包括插件的安装,在脚本中一次完成。 sudo apt-get update sudo apt-get -y install git python-pip python-virtualenv python-qt4-dev python3-pyqt4 libqt…
使用eclipse创建Struts2项目
eclipse版本: Kepler Service Release 1 http://www.eclipse.org/downloads/ struts版本:2.3.16 http://struts.apache.org/ 1.新建web项目 打开Eclipse,新建一个web项目"Struts2" 项目名字 勾选 web.xml选项 建好的…

8、进程通信-匿名管道
匿名管道 一个单向,未命名的管道,通常用来在一个父进程和一个子进程间传输数据。只能实现本地机器上两个进程间的通信,而不能实现跨网络的通信。 BOOL CreatePipe( PHANDLE hReadPipe, // read handle PHANDLE hWriteP…
Enhanced-RCNN: 一种高效的比较句子相似性的方法 |WWW 2020
作者 | 彭爽出品 | AI科技大本营(ID:rgznai100)国际顶级会议WWW2020将于4月20日至24日举行。始于1994年的WWW会议,主要讨论有关Web的发展,其相关技术的标准化以及这些技术对社会和文化的影响,每年有大批的学者、研究人…

直接可以拿去用的正则验证表达式
直接可以拿去用的正则验证表达式为了方便自己也方便初学的学弟们,自己总结了网上的众多正则验证式,现分享给大家,可以直接拿去用。一、校验数字的1 数字:^[0-9]*$2 n位的数字:^\d{n}$3 至少n位的数字:^\d{n…
家庭局域网开启AP隔离利用无线路由器互连
一开始可以上网,可以ping网关192.168.1.1,但是几台电脑之间就是不能互ping。 其实,真实的原因就是没有开启无线路由器的AP隔离。 在浏览器中输入192.168.1.1进入路由搜索一般用户名密码都是admin,具体请参见自己路由的说明书 操…
通过 Python 代码实现时间序列数据的统计学预测模型
来源 | DeepHub IMBA封图 | CSDN 付费下载于视觉中国 在本篇中,我们将展示使用 Python 统计学模型进行时间序列数据分析。 目标是:根据两年以上的每日广告支出历史数据,提前预测两个月的广告支出金额。原始数据:2017-01-01 到 201…

神色洋溢的 域名背后的故事
前短时间,我刚申请一个域名,好的顶级域名都被被人一拥而上的都强去了,我只好找那些申请好的用户买呀,这叫炒作,就是这样的抄起来的。你说平常一个也就100左右就搞定,可是现在要是到那票手里,那就…

Rust语言开发基础(六)基础语法
2019独角兽企业重金招聘Python工程师标准>>> 一、变量的定义和使用 其它常见的编程语言对变量的定义通常是通过声明类型和使用关键new来创建一个变量,但Rust不是,Rust使用关键字let。 1. 变量绑定通过let实现 fn main() { let x 5; } 2. 变量…
400 多行代码!超详细 Rasa 中文聊天机器人开发指南 | 原力计划
作者 | 无名之辈FTER责编 | 夕颜出品 | 程序人生(ID:coder_life)本文翻译自Rasa官方文档,并融合了自己的理解和项目实战,同时对文档中涉及到的技术点进行了一定程度的扩展,目的是为了更好的理解Rasa工作机制…
Linux配置SSH无密码登陆
可以使用“公钥私钥"认证的方式来进行ssh登录。 所谓 "公钥私钥"认证方式,就是首先在客户机上创建一对公钥和私钥,公钥文件:~/.ssh/id_rsa.pub; 私钥文件:~/.ssh/id_rsa 然后把公钥文件放到目标服务器…
Linux进程浏览器htop安装与使用
htop 是一个 Linux 下的交互式的进程浏览器,可以用来替换Linux下的top命令。当前具有按树状方式来查看进程,支持颜色主题,可以定制等特性。其实htop是top的加强版,增加了很多功能。 官网 http://hisham.hm/htop/ 下载地址http:/…