Upcoming Events
Workshop on Network and Systems Support for Games (NetGames 2009)
11/23 - 11/25 @ Paris, France

LOOP 2009
11/26 - 11/29  

EVA 2009
12/4 - 12/5 @ Buenos Aires, Argentina

ICIDS 2009 Interactive Storytelling
12/9 - 12/11 @ Guimarães, Portugal

More events...


Quick Stats
6348 people currently visiting GDNet.
2341 articles in the reference section.

Help us fight cancer!
Join SETI Team GDNet!



Link to us

Link to us

  Intel sponsors gamedev.net search:   

Resolution Independence in 2D Direct3D Games – An Alternative Approach


Introduction

We all understand that writing resolution-independent 2D games can be a lot more challenging than 3D games. This article outlines an approach I have been experimenting with recently that I believe is producing far better results than the more usual method of scaling vertices.

The following two screenshots are taken from an old platform game of mine. In both cases, the “native” resolution of the game is 1024x768, but the game is running at 1280x1024 on my LCD, so the graphics have been scaled up accordingly.

The first image is as a result of storing a scaling factor based on 1024/ActualScreenWidth, which all vertices are then multiplied by prior to being rendered.

The next image uses the alternative method that I describe in more detail below. Note the smoother main sprite, the lack of the edge error on the door and the cleaner looking stars on the boxes.

The Basic Idea

The basic idea is not complicated and is also no silver bullet – there are some obvious cons to this approach which I will discuss later on.

In essence, I am using a “fake” back buffer to render to, in the same dimensions as the game’s “native” resolution. At the end of the rendering look, I am then copying this onto the actual back buffer using IDirect3DDevice9::StretchRect().

StretchRect() appears to be able to do a much cleaner job of scaling an already composited image than the results generated from manually scaling vertices. It also avoids errors such as the slight inaccuracy in the door in the first screenshot above.

The Implementation

Here’s my GraphicsDevice interface, omitting all functions not actually relevant to this technique:

class GraphicsDevice
{
private:
   IDirect3DDevice9 *Device;
   IDirect3DSurface9 *BackSurface,*DrawSurface;

   RECT DestRect;

public:
   GraphicsDevice() : Direct3D(0),Device(0),DrawSurface(0) { }
   ~GraphicsDevice(){ Release(); }

   void Acquire(HWND Hw,const GraphicsOptions &Options);
   void Release();

   void Begin(D3DCOLOR Clear);
   void End();
};
You can see that in addition to my device pointer, I need to store two IDirect3DSurface9 pointers; one for the fake buffer we will create and one to hold onto the actual back buffer while we are rendering to the fake one.

We also calculate the destination rectangle when we create the device to save some processing every loop, so this becomes another member of GraphicsDevice.

GraphicsOptions is just a small struct that contains the resolution height and width, filtering options, vertical sync options, etc.

The first task is in Acquire(), where in addition to setting up the device, we also need to create the fake back buffer.

void GraphicsDevice::Acquire(HWND Hw,const GraphicsOptions &Options)
{
   // set up device as normal, code omitted
   R=Device->CreateRenderTarget(1024,768,D3DFMT_A8R8G8B8,
                                D3DMULTISAMPLE_NONE,0,FALSE,
                                &DrawSurface,NULL);

   if(FAILED(R)) throw Error("Unable to create draw surface");

   DestRect=CreateAspectRect(Params.BackBufferWidth,
                             Params.BackBufferHeight);
}
CreateAspectRect() is a helper function that just calculates the largest 4:3 rectangle that will fit on the current resolution. If the resolution is 4:3 as well, the resolution is returned as the rectangle, otherwise the rectangle is smaller than the resolution along one or the other dimension and the result is letterboxed on to the back buffer.
RECT CreateAspectRect(int Width,int Height)
{
   int Fx,Fy;
   float X,Y;

   Y=float(Height)/3;
   X=Y*4;

   if(Width>=int(X)){ Fx=int(X); Fy=Height; }
   else
      {
      X=float(Width)/4;
      Y=X*3;

      Fx=Width; Fy=int(Y);
      }

   int Ox=0,Oy=0;

   if(Fx<Width) Ox=(Width-Fx)/2;
   if(Fy<Height) Oy=(Height-Fy)/2;

   RECT R; SetRect(&R,Ox,Oy,Ox+Fx,Oy+Fy);

   return R;
}
Obviously if you prefer to stretch your game to the screen regardless of the aspect ratio, or give the user the option, changing this RECT is all that is required.

Hand in hand with the acquiring is the releasing, which is trivial:

void GraphicsDevice::Reset()
{
   if(DrawSurface){ DrawSurface->Release(); DrawSurface=0; }

   // release device as normal, code omitted
}
Finally we move on to implementing the Begin() and End() pair:
void GraphicsDevice::Begin(D3DCOLOR Clear)
{
   Device->GetRenderTarget(0,&BackSurface);
   Device->SetRenderTarget(0,DrawSurface);

   Device->Clear(0,NULL,D3DCLEAR_TARGET,Clear,0,0);

   Device->BeginScene();
}
Nothing complicated – we get a reference to the existing back buffer and store it for later, then we set our fake back buffer as the rendering target. Then we clear and BeginScene() like normal.

The rest of the program now does its rendering, pretending that we are running at 1024x768. There is no need to scale any images from their “natural” size, worry about clipping or even be aware at this point that the game is running at a different resolution.

Finally, we implement End():

void GraphicsDevice::End()
{
   Device->EndScene();

   RECT SourceRect=CreateSimpleRect(1024,768);

   Device->SetRenderTarget(0,BackSurface);
   BackSurface->Release();

   Device->Clear(0,NULL,D3DCLEAR_TARGET,D3DCOLOR_XRGB(0,0,0),0,0);
   Device->StretchRect(DrawSurface,&SourceRect,BackSurface,&DestRect,
                       D3DTEXF_NONE);

   Device->Present(NULL,NULL,NULL,NULL);
}
The idea is that we restore the original back buffer, ensuring that we call Release() on our store pointer since grabbing the back buffer increments its reference count.

We can then clear the real back buffer to black and use StretchRect() to copy our fake back buffer to the relevant destination rectangle on the real back buffer.

We can then Present() as normal.

The Downsides

There are a couple of issues with this approach, although I believe that the advantages outweigh these.

Firstly, on a PC with a slow fill-rate, running the game at 640x480 will give no speed boost over running at 1024x768.

Secondly, there is obviously some additional overhead in doing the extra StretchRect() every loop.

Lastly, if you are using a depth or stencil buffer in your 2D game, you’ll need to create a new buffer to go with your fake back buffer that matches its size as you’ll get into big trouble if your fake back buffer is larger than your actual screen resolution.

But in essence, these are all performance trade-offs in exchange for a better looking result and simpler rendering code. Whether it presents a project with an advantage depends very much on the type of graphics the project uses, but I’ve found for games using textured quads as sprites, this method is producing far nicer looking and more robust results.



  Printable version
  Discuss this article