Jump to content

Resolution Independence in 2D Direct3D Games – An Alternative Approach

resolution buffer void rectangle create device scaling vertices
using StretchRect() to scale a final frame to the proper resolution rather than manually scaling vertices - the upsides and downsides

4: Adsense

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.

Posted Image

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.

Posted Image

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®) 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


Note: GameDev.net moderates article comments.