Recording DirectX and OpenGL Rendered Animations

Introduction

While creating games and simulations, sometimes it is necessary to record the rendered content for offline viewing. This is especially unavoidable in cases where the actual rendering process is too complex and time consuming to repeat on demand. Also for creating the cut-scene movies it is necessary to record the rendered animation apriori and use them in the games.

In DirectX, the library function D3DXSaveSurfaceToFile() helps in saving a surface to an image file. For OpenGl, we could use glReadPixels() to read the rendered image bits and then can manually save them to an image file. While these suffice for single frame recording, no such easy way exists for recording sequence of frames continuously (or selectively). In other words, no library functions exist to record our complete rendered animation results.

In this regard, this article presents few classes that are helpful in creating movies out of DirectX and OpenGL animations. Movies can be created from DirectX and OpenGL rendered frames, selectively or continuously, using the classes CDxToMovie and CGLToMovie presented below. In general, a typical movie creation process involves complex tasks such as reading the bitmap content, selecting the frame rate settings, codec settings, initalizing the media streams, writing the streams etc... (for a detailed discussion on how to create movies from normal bitmap image sequences, please refer to the article Create Movie from HBitmap). The classes CDxToMovie and CGLToMovie presented here abstract out all such unnecessary complexity and provide easy to use interface with simple and straight forward methods as explained below.

Recording a Movie from DirectX Rendered Sequence

The class CDxToMovie can record DirectX rendered sequences into a movie file. This class uses DirectX 9.0 interfaces such as LPDIRECT3DSURFACE9 for its functionality and hence you should be using DirectX 9.0 SDK or its compatibles to use this class. Using this class is pretty straighforward as described below.

To start with, copy the files DxToMovie.h, RenderTarget.h, AviFile.h and AviFile.cpp from the DirectX source code of this article to your project directory and add them to your project. And add vfw32.lib to the linker input libraries. Once added to your project, you can access the CDxToMovie class from your code by #including the header file "DxToMovie.h".

The CDxToMovie constructor accepts various arguments such as the output movie filename, the movie frame width and height required, bits per pixel etc... as shown below.

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

You can either pass your own values for these parameters or use the default values (which should work fine for most cases). However, it should be noted that these are one time settings and cannot be changed later during the movie recording time. Each CDxToMovie corresponds to a different movie file and recreating a CDxToMovie object with same output file name would not append to the previous movie content but would overwrite it.

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

Once an object has been created for the CDxToMovie, the method CDxToMovie::OnCreateDevice() should be invoked on that object at the time of Direct3D device creation for your application. Similarily, CDxToMovie::OnLostDevice(), CDxToMovie::OnResetDevice() and CDxToMovie::OnDestroyDevice() should be called at times when the device is lost, reset and destroyed respectively. The prototypes of these functions are as shown below.

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

The functions OnCreateDevice() and OnDestroyDevice() accept a single parameter, the pointer to your application's Direct3D device object. OnLostDevice() takes no parameters, but OnResetDevice() requires a pointer to the surface description of your device's back buffer surface as a D3DSURFACE_DESC*. The CDxToMovie object uses the information provided in the D3DSURFACE_DESC to create an appropriate offscreen render target internally that can be used to record your application's rendering.

The actual recording is done by the functions CDxToMovie::StartRecordingMovie() and CDxToMovie::PauseRecordingMovie(). These two functions must be called for each frame between IDirect3DDevice9::BeginScene() and IDirect3DDevice9::EndScene() as shown below.

    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 back onto BackBuffer's Surface
    g_pd3dDevice->StretchRect(g_MovieRecorder.RecordingSurface(),
                              NULL,pBackSurface,
                              0,D3DTEXF_NONE);

    g_pd3dDevice->EndScene();

In the above code snippet, the g_MovieRecorder.StartRecordingMovie(g_pd3dDevice) redirects all the subsequent rendering onto the CDxToMovie's internal render target until the g_MovieRecorder.PauseRecordingMovie(g_pd3dDevice) is called, which would restore the render target back to its original surface. Since all the rendering is done on the CDxToMovie's internal render target, your application's back surface would not be having any valid content to display in your application window. This would not matter if your application is just creating the movie directly wihtout presenting any animation on the screen (for example, if you are creating a rendered cut-scene to be inserted later into a game). However, if you are recording the movie from an interactive game session, it would be bad if the screen is not updated with the latest rendered content (because CDxToMovie has stolen the content by redirecting the render target). To avoid this, you can optionally copy back the CDxToMovie's internal render target content onto your application's back surface using the method IDirect3DDevice9::StretchRect(), followed by the usual g_pd3dDevice->EndScene() and g_pd3dDevice->Present() calls that would present the updated contents of the back buffer onto the screen, keeping the screen up-to-date.

In case you want to selectively avoid few frames from being recorded into the movie, just do not call the g_MovieRecorder.StartRecordingMovie() and g_MovieRecorder.PauseRecordingMovie() (and the   correspoding g_pd3dDevice->StretchRect()) for those frames, and the animation would be directly rendered onto the screen (without being redirected to the CDxToMovie's internal render target).

The demo code supplied with this article presents a simple DirectX application that renders on the screen a triangle that moves as the mouse moves over the window, which would simulatenously be rendered and recorded into a movie file (named output.avi). To run the demo executable, make sure you have the MPG4 codec installed on your machine and that the directory has write permissions to create the output movie file. For more details on Codecs and FPS settings, please refer to the article Create Movie From HBitmap.

Recording a Movie from OpenGL Rendered Sequence

The class CGLToMovie can record OpenGL rendered sequences into a movie file. This is a very simple and straight forward class as explained below.

To start with, copy the files GLToMovie.h, AviFile.h and AviFile.cpp from the OpenGL source code of this article to your project directory and add them to your project. And add vfw32.lib to the linker input libraries. Once added to your project, you can access the CGLToMovie class from your code by #including the header file "GLToMovie.h".

The CGLToMovie constructor accepts various arguments such as the output movie filename, the movie frame width and height required, bits per pixel etc... as shown below.

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

You can either pass your own values for these parameters or use the default values (which should work fine for most cases). However, it should be noted that these are one time settings and cannot be changed later during the movie recording time. Each CGLToMovie corresponds to a different movie file and recreating a CGLToMovie object with same output file name would not append to the previous movie content but would overwrite it.

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

After creating an object of CGLToMovie, all that need be done to record the animation is to call CGLToMovie::RecordFrame() method for each frame before invoking the SwapBuffers(). The code snippet for this would look like below.

   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());

The function CGLToMovie::RecordFrame() internally uses the glReadPixels() method to read the content of the frame buffer and appends it the frame to the output movie file. In case you want to selecitvely avoid few frames from being recorded into the movie, just do not call the g_MovieRecorder.RecordFrame() method for those frames and they would be skipped from being append into the movie.

The demo code supplied with this article presents a simple OpenGL application that animates few sharks on the screen, which would simulatenously be rendered and recorded into a movie file (named output.avi). To run the demo executable, make sure you have the Cinepak codec installed on your machine and that the directory has write permissions to create the output movie file. For more details on Codecs and FPS settings, please refer to the article Create Movie From HBitmap.

Few Points to Note to avoid Errors

Conclusions

This articles presented few classes that are helpful in recording movies from DirectX and OpenGL rendered animations. The generated movie quality may vary depending on a variety of settings ranging from the video frame rate settings to the codec being used. The choice of the codec highly influences the quality of the output movie generated. For example, for some screen capture applications choosing a particular codec known as Windows Media Video 9 Screen codec should give optimal results in both quality and size, though the same may not be the case for high bit rate animation applications (Refer to the article Capturing the Screen for more details about capturing the screen programmatically). The above described classes use avi as the movie type. However, they can be used to create other types of movies such as wmv and mov with same ease.

By   

P.Gopalakrishna

Homepage    Other Articles by Gopalakrishna