65.9K
CodeProject 正在变化。 阅读更多。
Home

录制 DirectX 和 OpenGL 渲染的动画

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (25投票s)

2006年10月12日

9分钟阅读

viewsIcon

251849

downloadIcon

4988

解释了以编程方式录制 DirectX 和 OpenGL 渲染动画的方法。

Sample Image - SimulationRecording.jpg

目录

引言

在创建游戏和模拟时,有时需要录制渲染内容以供离线查看。在实际渲染过程过于复杂且耗时,无法按需重复的情况下,这一点尤其不可避免。此外,为了创建过场动画,有必要事先录制渲染动画并在游戏中使用它们。

在 DirectX 中,库函数 D3DXSaveSurfaceToFile() 有助于将表面保存到图像文件中。对于 OpenGL,我们可以使用 glReadPixels() 读取渲染的图像位,然后手动将它们保存到图像文件中。虽然这些足以用于单帧录制,但没有简单的方法可以连续(或选择性地)录制帧序列。换句话说,没有库函数可以录制我们完整的渲染动画结果。

在这方面,本文介绍了几个类,这些类有助于从 DirectX 和 OpenGL 动画中创建电影。使用下面介绍的 CDxToMovieCGLToMovie 类,可以从 DirectX 和 OpenGL 渲染的帧中选择性地或连续地创建电影。通常,典型的电影创建过程涉及复杂的任务,例如读取位图内容、选择帧速率设置、编解码器设置、初始化媒体流、写入流等……(有关如何从普通 HBitmap 图像序列创建电影的详细讨论,请参阅文章从 HBitmap 创建电影)。这里介绍的 CDxToMovieCGLToMovie 类抽象了所有这些不必要的复杂性,并提供了易于使用的接口,其方法简单明了,如下所述。

从 DirectX 渲染序列录制电影

CDxToMovie 类可以将 DirectX 渲染序列录制到电影文件中。该类使用 DirectX 9.0 接口(例如 LPDIRECT3DSURFACE9)来实现其功能,因此您应该使用 DirectX 9.0 SDK 或其兼容版本来使用该类。使用该类非常简单,如下所述。

首先,将本文 DirectX 源代码中的文件 DxToMovie.hRenderTarget.hAviFile.hAviFile.cpp 复制到您的项目目录中,并将它们添加到您的项目中。并将 vfw32.lib 添加到链接器输入库中。添加到项目后,您可以通过 #include 头文件 "DxToMovie.h" 从代码中访问 CDxToMovie 类。

CDxToMovie 构造函数接受各种参数,例如输出电影文件名、所需的电影帧宽度和高度、每像素位数等,如下所示。

 CDxToMovie(LPCTSTR lpszOutputMovieFileName = _T("Output.avi"),
       int nFrameWidth = GetSystemMetrics(SM_CXSCREEN),  /*Frame Width*/
       int nFrameHeight = GetSystemMetrics(SM_CYSCREEN), /*Frame Height*/
       int nBitsPerPixel = 32,     /*Bits per Pixel*/
       DWORD dwCodec = mmioFOURCC('M','P','G','4'),  /*Video Codec*/
       DWORD dwFrameRate = 1)      /*Frame Rate (FPS)*/

您可以为这些参数传递自己的值,也可以使用默认值(在大多数情况下应该可以正常工作)。但是,应该注意的是,这些都是一次性设置,不能在电影录制期间更改。每个 CDxToMovie 对应一个不同的电影文件,使用相同的输出文件名重新创建 CDxToMovie 对象不会追加到以前的电影内容,而是会覆盖它。

 CDxToMovie g_MovieRecorder("Output.Avi", 320, 240);

CDxToMovie 创建对象后,应在为应用程序创建 Direct3D 设备时在该对象上调用 CDxToMovie::OnCreateDevice() 方法。同样,当设备丢失、重置和销毁时,应分别调用 CDxToMovie::OnLostDevice()CDxToMovie::OnResetDevice()CDxToMovie::OnDestroyDevice()。这些函数的原型如下所示。

  class CDxToMovie
  {
    HRESULT OnCreateDevice(LPDIRECT3DDEVICE9 pd3dDevice);
    HRESULT OnDestroyDevice(LPDIRECT3DDEVICE9 pd3dDevice);
    HRESULT OnLostDevice();
    HRESULT OnResetDevice(LPDIRECT3DDEVICE9 pd3dDevice, 
                        const D3DSURFACE_DESC* pBackBufferSurfaceDesc);
  };

函数 OnCreateDevice()OnDestroyDevice() 接受一个参数,即指向应用程序 Direct3D 设备对象的指针。OnLostDevice() 不接受参数,但 OnResetDevice() 需要一个指向设备后缓冲区表面描述的指针,类型为 D3DSURFACE_DESC*CDxToMovie 对象使用 D3DSURFACE_DESC 中提供的信息在内部创建一个适当的离屏渲染目标,该目标可用于录制应用程序的渲染。

实际录制由函数 CDxToMovie::StartRecordingMovie()CDxToMovie::PauseRecordingMovie() 完成。这些函数必须在每个帧的 IDirect3DDevice9::BeginScene()IDirect3DDevice9::EndScene() 之间调用,如下所示。

  g_pd3dDevice->BeginScene();

  // Capture the Rendering onto CDxToMovie's Render Target
  g_MovieRecorder.StartRecordingMovie(g_pd3dDevice);
     // Render as usual.....    
     g_pd3dDevice->Clear(0,NULL,D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
                            D3DCOLOR_XRGB(0,0,200),1,0);
     g_pd3dDevice->SetStreamSource(0,g_pVB,0,sizeof(CUSTOMVERTEX));
     g_pd3dDevice->SetFVF(D3DFVF_CUSTOMVERTEX);
     g_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLELIST,0,1);
  g_MovieRecorder.PauseRecordingMovie(g_pd3dDevice);

  // Copy the CDxToMovie's Render Target content onto BackBuffer's Surface
  g_pd3dDevice->StretchRect(g_MovieRecorder.RecordingSurface(),
                              NULL,pBackSurface,
                              0,D3DTEXF_NONE);

  g_pd3dDevice->EndScene();

在上面的代码片段中,g_MovieRecorder.StartRecordingMovie(g_pd3dDevice) 将所有后续渲染重定向到 CDxToMovie 的内部渲染目标,直到调用 g_MovieRecorder.PauseRecordingMovie(g_pd3dDevice),这将渲染目标恢复到其原始表面。由于所有渲染都在 CDxToMovie 的内部渲染目标上完成,因此应用程序的后表面将没有任何有效内容可以在应用程序窗口中显示。如果应用程序只是直接创建电影而不在屏幕上呈现任何动画(例如,如果您正在创建要稍后插入游戏中的渲染过场动画),这并不重要。但是,如果您正在从交互式游戏会话录制电影,如果屏幕没有使用最新渲染内容进行更新,那将是不好的(因为 CDxToMovie 通过重定向渲染目标窃取了内容)。为了避免这种情况,您可以选择使用 IDirect3DDevice9::StretchRect() 方法将 CDxToMovie 的内部渲染目标内容复制回应用程序的后表面,然后是通常的 g_pd3dDevice->EndScene()g_pd3dDevice->Present() 调用,这将把后缓冲区的更新内容呈现到屏幕上,使屏幕保持最新。

如果您想选择性地避免将某些帧录制到电影中,只需不调用那些帧的 g_MovieRecorder.StartRecordingMovie()g_MovieRecorder.PauseRecordingMovie()(以及相应的 g_pd3dDevice->StretchRect()),动画将直接渲染到屏幕上(而不会重定向到 CDxToMovie 的内部渲染目标)。

本文提供的演示代码提供了一个简单的 DirectX 应用程序,该应用程序在屏幕上渲染一个三角形,该三角形随着鼠标在窗口上移动而移动,该三角形将同时渲染并录制到电影文件(名为 output.avi)中。要运行演示可执行文件,请确保您的机器上安装了 MPG4 编解码器,并且该目录具有创建输出电影文件的写入权限。有关编解码器和 FPS 设置的更多详细信息,请参阅文章从 HBitmap 创建电影

从 OpenGL 渲染序列录制电影

CGLToMovie 类可以将 OpenGL 渲染序列录制到电影文件中。这是一个非常简单明了的类,如下所述。

首先,将本文 OpenGL 源代码中的文件 GLToMovie.hAviFile.hAviFile.cpp 复制到您的项目目录中,并将它们添加到您的项目中。并将 vfw32.lib 添加到链接器输入库中。添加到项目后,您可以通过 #include 头文件 "GLToMovie.h" 从代码中访问 CGLToMovie 类。

CGLToMovie 构造函数接受各种参数,例如输出电影文件名、所需的电影帧宽度和高度、每像素位数等,如下所示。

 CGLToMovie(LPCTSTR lpszOutputMovieFileName = _T("Output.avi"), 
       int nFrameWidth = GetSystemMetrics(SM_CXSCREEN),  /*Frame Width*/
       int nFrameHeight = GetSystemMetrics(SM_CYSCREEN), /*Frame Height*/
       int nBitsPerPixel = 24,  /*Bits per Pixel*/
       DWORD dwCodec = mmioFOURCC('M','P','G','4'),    /*Video Codec */
       DWORD dwFrameRate = 1)  /*Frames Per Second (FPS)*/

您可以为这些参数传递自己的值,也可以使用默认值(在大多数情况下应该可以正常工作)。但是,应该注意的是,这些都是一次性设置,不能在电影录制期间更改。每个 CGLToMovie 对应一个不同的电影文件,使用相同的输出文件名重新创建 CGLToMovie 对象不会追加到以前的电影内容,而是会覆盖它。

  CGLToMovie g_MovieRecorder("Output.Avi", VIEWPORTWIDTH, VIEWPORTHEIGHT);

创建 CGLToMovie 对象后,所有需要做的是在调用 SwapBuffers() 之前为每个帧调用 CGLToMovie::RecordFrame() 方法来录制动画。此代码片段如下所示。

   glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
   // Render as usual
   for (int i = 0; i < NUM_SHARKS; i++) 
   {
      glPushMatrix();
      FishTransform(&sharks[i]);
      DrawShark(&sharks[i]);
      glPopMatrix();
   }
   
   // Capture the Rendering into CGLToMovie's movie file
   g_MovieRecorder.RecordFrame();

   SwapBuffers(wglGetCurrentDC());

函数 CGLToMovie::RecordFrame() 内部使用 glReadPixels() 方法读取帧缓冲区的内容,并将其帧追加到输出电影文件。如果您想选择性地避免将某些帧录制到电影中,只需不调用那些帧的 g_MovieRecorder.RecordFrame() 方法,它们将跳过不被追加到电影中。

本文提供的演示代码提供了一个简单的 OpenGL 应用程序,该应用程序在屏幕上动画化几条鲨鱼,这些鲨鱼将同时渲染并录制到电影文件(名为 output.avi)中。要运行演示可执行文件,请确保您的机器上安装了 Cinepak 编解码器,并且该目录具有创建输出电影文件的写入权限。有关编解码器和 FPS 设置的更多详细信息,请参阅文章从 HBitmap 创建电影

为避免错误需要注意的几点

  • 在渲染应用程序中,通常可以更改渲染表面格式(或宽度和高度设置)并使用新设置继续动画。但是,一旦电影以其第一帧的特定宽度、高度和每像素位数设置开始,就无法在后续帧中更改这些设置。电影的所有帧都应具有相同的格式和大小。因此,要求您在录制电影时,限制应用程序渲染窗口的尺寸调整,或限制其设备和表面更改为不同的格式。一旦电影录制以特定设置和格式开始,整个电影都应以相同的设置和格式录制。
  • 如文章从 HBitmap 创建电影中所述,电影需要编解码器来压缩其帧。在本文提供的代码中,电影创建功能在 AviFile.h 和 AviFile.cpp 文件中处理。默认使用的编解码器是 MPG4,它必须安装在您的机器上才能成功创建电影文件。您可以修改 AviFile.cpp 中的代码以使用您自己选择的编解码器来创建电影。但是,如果您使用的编解码器未在系统上安装,或者您的帧大小设置不符合编解码器格式要求,那么在渲染电影时可能会遇到错误。(但是,由于电影创建库的容错性质,即使发生电影渲染错误,您的渲染应用程序仍将继续执行。)有关更多详细信息,请参阅上述文章。
  • 电影创建库使用函数指针来实现其功能。但是,由于 Visual Studio 2005 中的重大更改,Visual Studio .Net 编译器和最新的 Visual Studio 2005 编译器之间的函数指针赋值方式不同。对于 Visual Studio 2005,指向成员的指针现在需要限定名称和 & 符号。如果您在编译源代码时遇到此方面的错误,请参阅:Visual C++ 2005 编译器中的重大更改

结论

本文介绍了一些有助于从 DirectX 和 OpenGL 渲染动画中录制电影的类。生成的电影质量可能因各种设置而异,从视频帧速率设置到所使用的编解码器。编解码器的选择极大地影响了生成的输出电影的质量。例如,对于某些屏幕捕获应用程序,选择一种名为 Windows Media Video 9 Screen 编解码器 的特定编解码器应该在质量和大小方面都能提供最佳结果,尽管对于高比特率动画应用程序可能并非如此(有关以编程方式捕获屏幕的更多详细信息,请参阅文章捕获屏幕)。上述类使用 avi 作为电影类型。但是,它们可以同样轻松地用于创建其他类型的电影,例如 wmv 和 mov。

© . All rights reserved.