juhara.com
Language : English Indonesia

2D Animation with Direct3D Part 1

Zamrony P Juhara
18 September 2006 16:23:00
 (8553 views)
Tutorial about how to create 2D animation with Direct3D. This first article discusses basic of Direct3D and how to display 2D sprite

Why about 2D?

Because I am one of side scroller game fan such as Sega Sonic or Mario Bros.

Why Direct3D?

Since version 8, Microsoft has announced that DirectDraw is now deprecated and no longer being developed. Microsoft will focus on developing DirectX Graphics (sometime refer as Direct3D). i.e. new API for graphics where DirectDraw and Direct3D (Direct X version 7 or older) is united. Near in the future, there are good chances that DirectDraw is no longer supported. In this tutorial, all code use DirectX 8 (because my development system uses DirectX 8 , you must install DirectX 8 to be able to run demo that we will develop in this tutorial. I will use term Direct3D to refer DirectX Graphics.

Is it possible to develop 3D application with Delphi?

Of course! Download first DirectX header conversion units from SourceForge (DirectX 9) or here (DirectX 8.1). Source code is available to download here. Ok enough with the introduction. Let's get started..

Direct3D Basic

Direct3D Initialization

Get instance of IDirect3D8 interface

Direct3DCreate8() function is available for instantiate IDirect3D8 interface declared in DirectXGraphics.pas unit. For example :

var
  d3d8:IDirect3D8;
begin
  d3d8:=Direct3DCreate8(D3D_SDK_VERSION);
  ...

If it succeed, d3d8 will hold pointer to IDirect3D8 interface which later can be used to get IDirect3DDevice8 instance, interface thet represents graphics card installed on the system. IDirect3D8 also used for querying device capabilities.

Device Initialization

During application runtime, we will use device quite often, i.e, to initialize graphic mode, doing transformation, creating textures, display rendering result to screen and etc. Device is represented by IDirect3DDevice8 interface. To get pointer to this interface we call CreateDevice() of IDirect3D8 .

function CreateDevice(const Adapter : Cardinal;
                      const DeviceType : TD3DDevType;
                      hFocusWindow : HWND;
                      BehaviorFlags : LongWord;
                      var pPresentationParameters:
                          TD3DPresent_Parameters;
                      out ppReturnedDeviceInterface :
                          IDirect3DDevice8) : HResult; stdcall;
  • Adapter is ordinal value of graphic card. To refer to primary graphic card, use D3DADAPTER_DEFAULT.
  • DeviceType, type of device. Set to D3DDEVTYPE_HAL to use HAL.
  • hFocusWindow, window handle that receive notification.
  • BehaviorFlags, behaviour of device. Set with D3DCREATE_HARDWARE_VERTEX_PROCESSING to use graphics card supporting 3D acceleration or D3DCREATE_SOFTWARE_VERTEX_PROCESSING otherwise. D3DCREATE_HARDWARE_VERTEX_PROCESSING and D3DCREATE_SOFTWARE)VERTEX_PROCESSING mutually exclusive, i.e, we can only use one of them but not both.
  • pPresentationParameters containes information for presenting rendering result to the screen such as graphics mode, color depth, number of back buffer and etc.

    For now, the most important fields are: BackBufferWidth and BackBufferHeight. You use it to setup screen resolution for example 800x600, 1024x768 etc. If a screen resolution is not supported, CreateDevice will fail. Tips: It is recomended to use standard graphic mode because they are likely supported by most graphic card available today such 800x600 or 1024x768. Windowed, you use it to determined whether application running in windowed mode or fullcreen. If it is false application runs in fullscreen and occupies entire screen. In fullscreen mode, application can use page flipping or blitting to do double buffering process. In windowed mode, double buffering use blitting only.

  • Note: Page flipping or blitting is double buffering techniques commonly used. Double buffering was developed to minimize flicker to occur, i.e, by use of two buffers (or triple buffering for three buffers), i.e primary buffer, or so called front buffer, which is visible to us, and an additional invisible buffer called back buffer. All drawing is done at back buffer. After drawing is finished, back buffer is transferred to front buffer. Page flipping is technique to transfer back buffer into front buffer by changing front buffer into back buffer and vice versa by means of changing starting address of scan of graphic card. Blitting, abbreviation of Block Binary transfer, copies content of the back buffer to the front buffer. Theoritically page flipping is more faster than blitting, because transfering back buffer to front buffer does not involve moving any data around.

    BackBufferFormat, we use to decide color depth we want. For example, to run application with 32 bit color depth, we use D3DFMT_A8R8G8B8 where each pixels is consist of four components, Alpha(A) =8 bit, Red (R)= 8 bit, Green(G)= 8 bit and Blue(B)=8 bit. Alpha determines level of transparency of color if it is blended with other color. For 16 bit mode, we use D3DFMT_R5G6B5 with R=5 bit G=6 bit and B=5 bit without any alpha component. For 24 bit mode, you use constant D3DFMT_R8G8B8 without alpha.

    SwapEffect determines how swap of back buffer and front buffer occur. If it is set with D3DSWAPEFFECT_DISCARD, then after back buffer and front buffer is swap, content of back buffer is discarded so we should not assume that content of back buffer is same before it is swapped. With D3DSWAPEFFECT_DISCARD, we tell DirectX to pick the most optimal swap mechanism for our buffer. If we set it with D3DSWAPEFFECT_FLIP, we tells DirectX to use page flipping mechanism and if we use D3DSWAPEFFECT_COPY or D3DSWAPEFFECT_VSYNC_COPY, DirectX uses blitting. VSYNC blits by synchronize it with vertical trace of monitor's electron gun to minimize tear-off effect.

    hDeviceWindow, window handle of render target. If we set to 0, hFocusWindow is used. ppReturnedDeviceInterface, variable that will receive pointer to IDirect3DDevice8 interface.

If CreateDevice() failed, ppReturnedDeviceInterface is equal to nil. For example:

if accel3DSupported then
  behaviour:=D3DCREATE_HARDWARE_VERTEX_PROCESSING 
else
  behaviour:=D3DCREATE_SOFTWARE_VERTEX_PROCESSING;

ZeroMemory(presentParam,sizeof(TD3DPresent_Parameters));

presentParam.hFocusWindow:=Form1.Handle;
presentParam.BackBufferCount:=1;
presentParam.BackBufferWidth:=800;
presentParam.BackBufferHeight:=600;
presentParam.SwapEffect:=D3DSWAPEFFECT_DISCARD;
presentparam.BackBufferFormat:=D3DFMT_A8R8G8B8;

d3d8.CreateDevice(D3DADAPTER_DEFAULT,
                  D3DDEVTYPE_HAL,
                  behaviour,
                  presentParam,
                  d3d8Dev); 

Checking for 3D acceleration availability.

To be able to utilize graphic card capability optimally, we need to ensure that graphic card support 3D acceleration. To check graphic card capabilities, IDirect3D8 has GetDeviceCaps() method.

function GetDeviceCaps(const Adapter : Cardinal;
                       const DeviceType : TD3DDevType;
                       out pCaps : TD3DCaps8) : HResult; stdcall;

Adapter, ordinal value of graphic card, 0=first graphic card, 1=second,..etc. As usual, we can fill it with D3DADAPTER_DEFAULT to refer to primary adapter.

DeviceType, device type can be D3DDEVTYPE_HAL or D3DDEVTYPE_REF.

pCaps, graphic card capability information. To check whether 3D accelerator is available we need to check pCaps.DevCaps for D3DDEVCAPS_HWTRANSFORMANDLIGHT value. If it contains this value, then 3D acceleration is available. For example:

d3d8.GetDeviceCaps(D3DADAPTER_DEFAULT, 
                   D3DDEVTYPE_HAL, 
                   aCaps);

accel3DSupported:=(aCaps.DevCaps AND                            
                   D3DDEVCAPS_HWTRANSFORMANDLIGHT 
                   =  D3DDEVCAPS_HWTRANSFORMANDLIGHT);

Clear Back Buffer

To clear buffer content, whether it is rendering buffer, depth buffer or stencil buffer, we use IDirect3DDevice8 method Clear().

function Clear(const Count : LongWord; 
               pRects : PD3DRect; 
               const Flags : LongWord; 
               const Color : TD3DColor;
               const Z : Single;  
               const Stencil : LongWord) : HResult; stdcall;

Count holds number of rectangle in pRects. If pRects=nil then Count must be set to 0.

pRects holds rectangles to be cleared. If it is set to nil, then all buffer will be cleared.

Flags indicates type of buffer that will be cleared. D3DCLEAR_STENCIL causes stencil buffer to be cleared with value in Stencil parameter. D3DCLEAR_ZBUFFER causes depth buffer to be cleared with value in Z and D3DCLEAR_TARGET causes render target is cleared with color information stored in Color parameter. For now, depth buffer and stencil buffer are not being discussed.

To clear (or more precise, to fill) all render buffer with red color (ARGB=$FFFF0000) :

d3dDev.Clear(0,nil, D3DCLEAR_TARGET, $FFFF0000,0,0);

Begin and End 3D Object Drawing.

Before drawing 3D, we must call BeginScene() and end it with EndScene() call, where between both method callis where you put your drawing code.

d3ddev.BeginScene;
//draw your objects here
d3ddev.EndScene;

BeginScene/EndScene pair must be called even though we are not drawing anything.

Presenting Back Buffer content To Front Buffer

To make all objects we have drawn visible, we must call Present(), a method of IDirect3DDevice8.

function Present(pSourceRect, pDestRect : PRect; 
                 const hDestWindowOverride : HWND; 
                 pDirtyRegion : PRgnData) : HResult; stdcall;
  • pSourceRect, source rectangle. If we set to nil , entire back buffer is copied. Device must be create with swap effect D3DSWAPEFFECT_COPY or D3DSWAPEFFECT_VSYNC_COPY, otherwise it must be set to nil.
  • pDestRect, destination rectangle. It is similar to pSourceRect.
  • hDestWindowOverride, window handle of render target. If it is 0 then TD3Dpresent_Parameters.hDeviceWindow will be used.
  • pDirtyRegion is not used and must be set to nil.

Example:

d3ddev.Present(nil,nil,nil,0,nil);

Texture

Texture is image that wrapped on polygon to enhance realism of 3D object. In Direct3D 8, texture is represented in IDirect3DTexture8 interface.

Creating Texture from Image File

DirectX has utilities called D3DX, two of functions available in D3DX are D3DXCreateTextureFromFile() and D3DXCreateTextureFromFileEx(). Both are declared in d3dx8.pas unit. Later function is an extension to the previous one and we will use it to create textures.

Sprite

Sprite is term in game programming to refer to image of characters in a game. Main property of sprite is transparency information where transparent color (color key) will not be drawn. Mouse cursor is an example of sprite.

ID3DXSprite

D3DX provides interface to simplify sprite manupulation with ID3DXSprite.

Creating Sprite

We use D3DXCreateSprite() function to get pointer of ID3DXSprite instance.

function D3DXCreateSprite(const pDevice : IDirect3DDevice8; 
                        out ppSprite : ID3DXSprite) : HResult; stdcall;

Example:

D3DXCreateSprite(d3dDev,aSprite);

Begin and End Sprite Drawing

To begin and to end drawing, we must call ID3DXSprite.Begin and ID3DXSprite.End. Because Begin and End is reserved words in Pascal language, so in conversion header, this function was renamed to _Begin and _End

aSprite._Begin; 
//do something with sprite 
aSprite._End;

Animation Application Implementation

Enough for Direct3D introduction, let us create framework implementation for 2D application. We are going to create an application where we can move Guile character (Street Fighter), control Guile to perform punching and kicking action.

TEngine

We are going to create wrapper class for Direct3D initialization and rendering. Features that this class provides:

  • Method for device initialization which is implemented in InitDevice.
  • Clearing buffer wtih Clear.
  • Method for displaying rendering result.
  • Method for drawing 3D objects which is implemented by abstract method, Draw. Derived classes should implement this method.

T3DCollection

TD3DCollection is base class for collection of items in this 3D engine. This class has a property of type TEngine and is derived from TCollection.

Texture wrapper class

Texture management encapsulation are consist of two classes: TTextureCollection and TTexture. TTexture handles initialization of IDirect3DTexture8 and image file loading. TTextureCollection handles TTexture management. TTextureCollection is derived from T3DCollection. TTexture is derived from TCollectionItem.

unit u_3D_engine;

interface
uses classes,windows,sysutils,directxGraphics;

type

  E3DError=class(Exception);

  TEngineParam=record
     Handle:HWND;
     Fullscreen:boolean;
     ResolutionWidth:integer;
     ResolutionHeight:integer;
     TotalBackBuffer:integer;
     ColorDepth:integer;
     InitDeviceOnCreate:boolean;
  end;

  TEngine=class(TObject)
  private
    FDirect3DObj:IDirect3D8;
    FDirect3DDevice:IDirect3DDevice8;

    function CreateD3D:IDirect3D8;
    function CreateDevice(const d3d:IDirect3D8):IDirect3DDevice8;
    function HwVertexProcSupported:boolean;
  protected
    FEngineParam:TEngineParam;
    FPresentParam:TD3DPresent_Parameters;

    procedure BeginDraw;
    procedure Draw;virtual;abstract;
    procedure EndDraw;
  public
    constructor Create(Param:TEngineParam);virtual;
    destructor Destroy;override;

    procedure InitDevice;

    procedure Clear(const ClearCol:TD3DColor);
    procedure Render;
  published
    property Direct3DObj:IDirect3D8 read FDirect3DObj;
    property Direct3DDevice:IDirect3DDevice8 read FDirect3DDevice;
  end;

  T3DCollection=class(TCollection)
  private
    FEngine: TEngine;
    procedure SetEngine(const Value: TEngine);
  published
    property Engine:TEngine read FEngine write SetEngine;
  end;

  T3DObject=class(TCollectionItem)
  private
  protected
    FVertexBuff:IDirect3DVertexBuffer8;
    FIndexBuff:IDirect3DIndexBuffer8;
  public
    procedure Init;virtual;abstract;
    procedure Draw;virtual;abstract;
  end;

  TTexture=class(TCollectionItem)
  private
    FTextureObj: IDirect3DTexture8;
    FColorKey: TD3DColor;
    procedure SetColorKey(const Value: TD3DColor);
  public
    destructor Destroy;override;
    procedure LoadFromFile(const filename:string);
  published
    property TextureObj:IDirect3DTexture8 read FTextureObj;
    property ColorKey:TD3DColor read FColorKey write SetColorKey;
  end;

  TTextureCollection=class(T3DCollection)
  end;

implementation
uses d3dx8;

{ TEngine }

procedure TEngine.BeginDraw;
begin
  if FDirect3DDevice<>nil then
    FDirect3DDevice.BeginScene;
end;

procedure TEngine.Clear(const ClearCol:TD3DColor);
begin
  if FDirect3DDevice<>nil then
    FDirect3DDevice.Clear(0,nil,D3DCLEAR_TARGET,ClearCol,0,0);
end;

constructor TEngine.Create(Param: TEngineParam);
begin
  FEngineParam:=Param;
  FDirect3DObj:=CreateD3D;
  if FEngineParam.InitDeviceOnCreate then
    InitDevice;
end;

function TEngine.CreateD3D: IDirect3D8;
begin
  result:=Direct3DCreate8(D3D_SDK_VERSION);
end;

function TEngine.CreateDevice(const d3d: IDirect3D8): IDirect3DDevice8;
var hr:HResult;
   vertexProcessing:cardinal;
begin
  if d3d<>nil then
  begin
    ZeroMemory(@FPresentParam,sizeof(TD3DPresent_Parameters));

    FPresentParam.Windowed:=not FEngineParam.Fullscreen;
    FPresentParam.BackBufferWidth:=FEngineParam.ResolutionWidth;
    FPresentParam.BackBufferHeight:=FEngineParam.ResolutionHeight;
    FPresentParam.BackBufferCount:=FEngineParam.TotalBackBuffer;
    FPresentParam.BackBufferFormat:=FEngineParam.ColorDepth;
    FPresentParam.SwapEffect:=D3DSWAPEFFECT_DISCARD;

    //3D hardware acceleration tersedia?
    if HwVertexProcSupported then
      vertexProcessing:=D3DCREATE_HARDWARE_VERTEXPROCESSING
    else
      vertexProcessing:=D3DCREATE_SOFTWARE_VERTEXPROCESSING;

    hr:=d3d.CreateDevice(D3DADAPTER_DEFAULT,
                   D3DDEVTYPE_HAL,
                   FEngineParam.Handle,
                   vertexProcessing,
                   FPresentParam,
                   result);
    if result=nil then
      raise E3DError.Create(D3DXErrorString(hr));
  end else
   result:=nil;

end;

destructor TEngine.Destroy;
begin
  FDirect3DDevice:=nil;
  FDirect3DObj:=nil;
  inherited;
end;

procedure TEngine.EndDraw;
begin
  if FDirect3DDevice<>nil then
    FDirect3DDevice.EndScene;
end;

function TEngine.HwVertexProcSupported: boolean;
var caps:TD3DCaps8;
begin
  FDirect3DObj.GetDeviceCaps(D3DADAPTER_DEFAULT,
                    D3DDEVTYPE_HAL,caps);

  result:=((caps.DevCaps and D3DDEVCAPS_HWTRANSFORMANDLIGHT)0);
end;

procedure TEngine.InitDevice;
begin
  FDirect3DDevice:=CreateDevice(FDirect3DObj);
end;

procedure TEngine.Render;
begin
  BeginDraw;
  try
    Draw;
    if FDirect3DDevice<>nil then
      FDirect3DDevice.Present(nil,nil,0,nil);
  finally
    EndDraw;
  end;
end;

{ T3DCollection }

procedure T3DCollection.SetEngine(const Value: TEngine);
begin
  FEngine := Value;
end;

{ TTexture }

destructor TTexture.Destroy;
begin
  FTextureObj:=nil;
  inherited;
end;

procedure TTexture.LoadFromFile(const filename: string);
var atexcollection:TTextureCollection;
    hr:HResult;
begin
  if Collection is TTextureCollection then
  begin
    aTexCollection:=Collection as TTextureCollection;
    if (aTexCollection.Engine.Direct3DDevice<>nil) and
       fileExists(filename) then
    begin
      hr:=D3DXCreateTextureFromFileEx(aTexCollection.Engine.Direct3DDevice,
                        PChar(filename),
                        D3DX_DEFAULT,
                        D3DX_DEFAULT,
                        D3DX_DEFAULT,
                        0,
                        D3DFMT_UNKNOWN,
                        D3DPOOL_MANAGED,
                        D3DX_FILTER_NONE,
                        D3DX_FILTER_NONE,
                        FColorKey,
                        nil,nil,
                        FTextureObj);
       if FTextureObj=nil then
        raise E3DError.Create(D3DXErrorString(hr));
    end;
  end;
end;

procedure TTexture.SetColorKey(const Value: TD3DColor);
begin
  FColorKey := Value;
end;

end.

2D Engine Implementation

OK, next we create implementation to handle sprites. Sprite management will be encapsulated in TSprite class, while TSprite is managed by TSpriteCollection. Just like TTexture and TTextureCollection.

unit u2DSpr;

interface

uses classes,directxgraphics,d3dx8,u_3D_engine;

type
  TSpriteCollection=class(T3DCollection)
  private
     FSprite:ID3DXSprite;
     procedure Init;
  public
     destructor Destroy;override;
     procedure BeginDraw;
     procedure EndDraw;
  published
     property SpriteObj:ID3DXSprite read FSprite;
  end;

  TSprite=class(T3DObject)
  private
    FSprite:ID3DXSprite;
    FTexture: TTexture;
    FRotation: single;
    FColor: TD3DColor;
    FScaling: TD3DXVector2;
    FRotationCenter: TD3DXVector2;
    FTranslation: TD3DXVector2;
    FAlpha: byte;

    procedure SetTexture(const Value: TTexture);
    procedure SetColor(const Value: TD3DColor);
    procedure SetRotation(const Value: single);
    procedure SetRotationCenter(const Value: TD3DXVector2);
    procedure SetScaling(const Value: TD3DXVector2);
    procedure SetTranslation(const Value: TD3DXVector2);
    procedure SetAlpha(const Value: byte);
  public
    constructor Create(ACollection: TCollection);override;
    destructor Destroy;override;
    procedure Init;override;
    procedure Draw;override;

    procedure DrawEx(const translVect,
                              scaleVect,
                              rotCenter:PD3DXVector2;
                        const rot:single;
                        const Acolor:TD3DColor;
                        const alphaValue:byte);

    published
       property Texture:TTexture read FTexture write SetTexture;
       property Translation:TD3DXVector2 read FTranslation write SetTranslation;
       property X:single read FTranslation.x write FTranslation.x;
       property Y:single read FTranslation.y write FTranslation.y;
       property RotationCenter:TD3DXVector2 read FRotationCenter write SetRotationCenter;
       property Scaling:TD3DXVector2 read FScaling write SetScaling;
       property Rotation:single read FRotation write SetRotation;
       property Color:TD3DColor read FColor write SetColor;
       property Alpha:byte read FAlpha write SetAlpha;
    end;

implementation

{ TSprite }
constructor TSprite.Create(ACollection: TCollection);
var asprCollect:TSpriteCollection;
begin
  inherited;
  FSprite:=nil;
  if ACollection is TSpriteCollection then
  begin
    asprCollect:=ACollection as TSpriteCollection;
    if asprCollect.SpriteObj=nil then


      asprCollect.Init;

    FSprite:=asprCollect.SpriteObj;
  end;

  FTranslation.x:=0;
  FTranslation.Y:=0;

  FRotationCenter.x:=0;
  FRotationCenter.y:=0;
  FRotation:=0;

  FScaling.x:=1;
  FScaling.y:=1;
  FColor:=$FFFFFFFF;
end;

destructor TSprite.Destroy;
begin
  FSprite:=nil;
  inherited;
end;

procedure TSprite.Draw;
begin
  DrawEx(@FTranslation,
         @FScaling,
         @FRotationCenter,
         FRotation,
         FColor,
         FAlpha);
end;

procedure TSprite.DrawEx(const translVect, scaleVect,
  rotCenter: PD3DXVector2; const rot: single;
  const AColor:TD3DColor;const AlphaValue:byte);
begin
  if (FSprite<>nil) and
     (FTexture<>nil) and
     (FTexture.TextureObj<>nil) then
  begin
    FSprite.Draw(FTexture.TextureObj,nil,
                 scaleVect,
                 rotCenter,rot,
                 translVect,
                 (AlphaValue shl 24) or AColor);
  end;
end;

procedure TSprite.Init;
begin
end;

procedure TSprite.SetAlpha(const Value: byte);
begin
  FAlpha := Value;
end;

procedure TSprite.SetColor(const Value: TD3DColor);
begin
  FColor := Value;
end;

procedure TSprite.SetRotation(const Value: single);
begin
  FRotation := Value;
end;

procedure TSprite.SetRotationCenter(const Value: TD3DXVector2);
begin
  FRotationCenter := Value;
end;

procedure TSprite.SetScaling(const Value: TD3DXVector2);
begin
  FScaling := Value;
end;

procedure TSprite.SetTexture(const Value: TTexture);
begin
  FTexture := Value;
end;

procedure TSprite.SetTranslation(const Value: TD3DXVector2);
begin
  FTranslation := Value;
end;

{ TSpriteCollection }

procedure TSpriteCollection.BeginDraw;
begin
  if FSprite<>nil then
    FSprite._Begin;
end;

destructor TSpriteCollection.Destroy;
begin
  FSprite:=nil;
  inherited;
end;

procedure TSpriteCollection.EndDraw;
begin
  if FSprite<>nil then
    FSprite._End;
end;

procedure TSpriteCollection.Init;
begin
  if (Engine<>nil) and (Engine.Direct3DDevice<>nil) then
    D3DXCreateSprite(Engine.Direct3DDevice,FSprite)
  else
    FSprite:=nil;
end;

end.

Below is screen shot of application.

Screen shot aplikasi

To move Guile, use right/left arraw, for kicking and punching action press J, K or L. To exit press ESC or ALT+F4. Source code can be downloaded here

Sprite images can be downloaded from www.gsarchive.net (however this site is dead now). Guile character is property of Marvel Comic, used here for educational purpose only.

Related Article

Do you like this article? Help this website improve by donating. Any amounts is appreciated.

Or you can help by bookmarking this page. Delicious Bookmark this on Delicious