viksoe.dk

UI: Mixing GDI and 3D Animations


This article was submitted .


If you haven't already viewed my Windowless UI sample you missed out on a really cool show. In this sample I have added a small 3D render engine which shows neat 3D animations and page transitions. While the framework itself works directly with Windows GDI, whenever a new window or event occurs, the window can request a 3D animation powered by DirectX to be played on the window canvas.

About DirectX

The DirectX library is one of the most evolving COM libraries Microsoft has put out to date. Almost every version iteration has significant changes to the API. Classes and methods come and go all the time. This is a side-effect of Microsoft trying to keep up with the raging 3D rendering technology and the pursuit for delivering ever-faster 3D games.

For quite some time, the DirectX library was reserved for fullscreen 3D games only. However, over time the graphics cards have gotten so clever that they are able to accelerate most operations on a windowed canvas as well.
The DirectDraw API was introduced in one of the early versions of DirectX and added some capability to mix GDI with an accelerated 3D surface. This was most notable in the GetDC method which would transform a 3D surface into a compatible GDI Device Context and allow large parts of the GDI functions (DrawText(), FillRect() etc) to work on it. Unfortunately many of the operations weren't accelerated on the card and thus using the APIs could be painfully slow.
During the succeeding versions, the DirectDraw API was slowly faded out of DirectX altogether and eventually mixing GDI & 2D with 3D just wasn't possible. Then, almost by magic, GetDC was re-introduced in DirectX 9 - most likely because Microsoft needed to take up the battle with the recent developments in the Mac OS desktop and its superb integration with OpenGL. It also turns out that by now most graphics cards are capable of accelerating windowed operations just fine.

Using DirectX

I'm not going to present a tutorial on creating a DirectX application. You can find plenty of good tutorials on the web. Since DirectX is so burgeoning, it is important to read samples that are written for version 9, though.
While setting up DirectX in windowed mode is trivial, there are several implications. Most important, you inherit the display format of the Windows desktop. You can request a more suitable format for your surfaces, but most likely you will face trouble when trying to mix it with GDI. This generally means that you can only use the scarce 3D operations, the GDI integration - but may not be able to LockRect the surface to do pixel manipulations.

The general procedure for mixing DirectX with a GDI window in my sample is:

  • Setup DirectX in windowed mode and take over a desktop window
  • Create an offscreen backdrop surface with the dimensions and display format of the window
  • Use Surface::GetDC to get a GDI version of the surface and paint the entire window on the DC
  • Prepare the sprites that will animate on the surface
  • Enter the render loop...
  •   Copy backdrop surface to display
  •   Render sprites

Creating a backdrop

We create a copy of the contents of the desktop window to a DirectX surface, because while we are in the render loop we'll continuously copy the backdrop back to the render surface. A normal DirectX application would call Device::Clear to reset the screen to black, but we'll give the impression that the desktop window is all intact while we're animating sprites on the window.
With this illusion we turn a normal GDI desktop window into a 3D rendered target surface without the user even knowing. We can safely animate 3D objects on the DirectX surface and then silently restore the GDI window when we're done.
Because of various limitations on the size of surfaces in graphics cards, we'll create the backdrop surface in System Memory. This allows an arbitrary size, but adds the risk of blit accelerations being unsupported on some 2nd generation cards.

There are several approaches to making a copy of the GDI window. The reason that I mentioned the GetDC method earlier is that without this method it gets slightly complicated and we would have to deal with the myriads of display formats. Instead we can now call this method, retrieve a Windows HDC and use the WM_PRINTCLIENT message to allow the desktop window to fill out the surface.

Preparing sprites

The idea with the render engine in the sample, is that it takes elements from the window (a header text or an icon) and animates them. Animations are displayed as short page transitions (fades or zooms in and out).
The DirectX 9 requires us to treat animating sprites in strictly 3D geometry. We'll create a polygon (wireframe) and put a texture on it. We can animate the sprite simply by stretching or rotating the coordinates of the polygon. DirectX will make sure to paint the texture in the correct perspective as long as we make an effort to calculate all coordinate changes on all 3 axes (x, y and z) in 3D space.

Many older generation 3D graphics cards have some nasty restrictions when it comes to creating texture bitmaps. Some cards only allow texture dimensions of the power of 2 (32, 64, 128...), some restrict the size to 256 pixels or less, and many only allow textures to be square in size.
To overcome these problems, each sprite may be constructed from several square textures (tiles). This makes it slightly more complicated to manage properly, but with a little tinkering we should be fine. Each tile contains a separate polygon and is mapped out with a 32, 64 or 128 pixel sized square texture on it. We keep a 1:1 ratio of the backdrop image and the texture, so we can put the sprite exactly over the original graphics without scaling the bitmap data.

Animating

Drawing the sprites are done using the standard DirectX DrawPrimitive call and with the use of vertex buffers. Coordinate transformations and rotation is done in code, rather than allowing the Direct3D accelerated matrix calculations. This is needed because we want to be absolutely sure that we can place a sprite exactly over the existing graphics - without tearing or pixel-compression errors.

Each animation is described in a simple from/to state diagram. A sprite will animate from a certain position or rotation back to its original position. The animation is scheduled to take a number of milliseconds. During this period each frame animated will smoothly interpolate transformations (horizontal and vertical movement) and rotations from one end-point to another. When no more animations are scheduled, the DirectX layer is shut down and normal GDI processing takes over.

It is important to note a severe limitation of this model: that while we're animating, the normal WM_PAINT processing is short-circuited and Windows is not given the chance to update the Window canvas. The immediate effect is that any text caret blinking or keypress will not display on the screen. Therefore, we cancel the animations straight away if the user presses a mouse-button or key.
It would be possible, but probably slow, to repeat the process of generating the backdrop surface (background) from time to time, allowing GDI and WM_PAINT messages to shine through.

The frame rate is currently controlled by Windows and its message pump. While normal DirectX animations attempt to animate frames whenever the message pump is idle, this sample simply uses an InvalidateRect call to have Windows generate WM_PAINT messages at a fast pace. Seems to work satisfactory on the Windows versions I've tested so far.

Notes

In the sample provided for download, all code related to the actual animation system is located in the file UIAnim.cpp. The code is being called from the WM_PAINT message handler located in the UIControls.cpp file.

To run the sample you need to have DirectX 9 installed. If no animations show up, the application has deemed your graphics card too slow to provide smooth animations.

Source Code Dependencies

Microsoft Visual C++ 6.0
Microsoft DirectX 9 SDK

Download Files

DownloadSample (92 Kb)
Source Code (127 Kb)

To the top