Sol's dirty DX7 tutorial

Textures

In this final chapter we'll scratch the surface on the subject of textures. Using textures is perhaps the most complex thing in d3d (the competing thing being the use of matrices). Complete information on textures would include multitexturing, colorkeying, blending, 'video' textures (rendering video to a texture) etc. But as before we'll just do things easiest possible way.

#define WIN32_LEAN_AND_MEAN		
#include <windows.h> // windows stuff
#include <stdio.h>   // standard IO
#include <stdlib.h>  // standard C lib
#include <ddraw.h>   // DirectDraw
#include <d3d.h>     // Direct3D
#include <d3dx.h>    // Direct3DX

/* 
 * Griddy3
 * d3dx7 single threaded app
 * sol/trauma 1999
 */

char progname[]="Griddy3 - Sol";
HWND mainhWnd;
HINSTANCE mainhInst;
LPD3DXCONTEXT dxctx;
LPDIRECT3DDEVICE7 d3dd;
LPDIRECT3D7 d3d;
LPDIRECTDRAWSURFACE7 myTexture;

LRESULT CALLBACK WindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam) {
  if (uMsg==WM_DESTROY) {
    d3d->Release();
    d3dd->Release();
    D3DXUninitialize();
    exit(wParam);
  }
  return DefWindowProc(hwnd,uMsg,wParam,lParam);
}

#define GRID_X 64
#define GRID_Y 48
#define XMUL (640.0f/(GRID_X))
#define YMUL (480.0f/(GRID_Y))

LPDIRECT3DVERTEXBUFFER7 vbuf;
WORD idx[2*(GRID_X+1)*(GRID_Y+1)];

void init(void) 
{
  D3DTLVERTEX *buf;
  D3DVERTEXBUFFERDESC vbd;
  vbd.dwSize=sizeof(vbd);
  vbd.dwCaps=D3DVBCAPS_WRITEONLY;
  vbd.dwFVF=D3DFVF_TLVERTEX;
  vbd.dwNumVertices=(GRID_X+1)*(GRID_Y+1);        
  d3d->CreateVertexBuffer(&vbd, &vbuf,0);

  vbuf->Lock(DDLOCK_WAIT|DDLOCK_WRITEONLY|DDLOCK_NOOVERWRITE,(void**)&buf,NULL);

   for (int y=0;y<(GRID_Y+1);y++)
    for (int x=0;x<GRID_X+1;x++) {
      
      idx[y*(GRID_X+1)*2+x*2+1]=(WORD)((y)*(GRID_X+1)+x);
      idx[y*(GRID_X+1)*2+x*2]=(WORD)((y+1)*(GRID_X+1)+x);
      
      buf[y*(GRID_X+1)+x].sx=x*XMUL;
      buf[y*(GRID_X+1)+x].sy=y*YMUL;
      buf[y*(GRID_X+1)+x].sz=0;
      buf[y*(GRID_X+1)+x].rhw=1;
      buf[y*(GRID_X+1)+x].color=0;
    }
    
  vbuf->Unlock();
}

void setup(void)
{ 
  D3DTLVERTEX *buf;

  vbuf->Lock(DDLOCK_WAIT|DDLOCK_WRITEONLY|DDLOCK_NOOVERWRITE,(void**)&buf,NULL);

  float i=GetTickCount()*0.02f;
  for (int y=0,c=0;y<GRID_Y+1;y++)
    for (int x=0;x<GRID_X+1;x++,c++) {     
      buf[c].tu=(-sin((y+i*0.394)*0.12251474)+cos((x-i*0.202)*0.12654374));
      buf[c].tv=(-cos((x+i*0.21)*0.12123474)+sin((-y+i*0.398)*0.12452474));      
      buf[c].color=RGB((int)(
                       sin((y+i)*0.2251474)*44+
                       cos((y-i)*0.2354128)*
                       cos((x+i)*0.3434913)*84
                       )+128,
                       (int)(
                       cos((y-i)*0.2252474)*44+
                       cos((x-i)*0.3334923)*84
                       )+128,
                       (int)(
                       sin((y-i)*0.1453474)*44+
                       sin((y+i)*0.2354328)*
                       cos((x-i)*0.3234933)*84
                       )+128);
    }
  vbuf->Unlock();
}

void reindeer(void) 
{
  static int inited=0;

  if (!inited) {
    init();
    inited++;
  }
  setup();

  //dxctx->Clear(D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER);

  d3dd->BeginScene();

  d3dd->SetTexture(0,myTexture); 

  for (int y=0;y<GRID_Y;y++)
    d3dd->DrawIndexedPrimitiveVB(D3DPT_TRIANGLESTRIP, vbuf, 
                                 0,(GRID_X+1)*(GRID_Y+1), 
                                 (idx+y*(GRID_X+1)*2),
                                 (GRID_X+1)*2,0);

  d3dd->SetTexture(0,NULL); 

  d3dd->EndScene();
}


int create_texture(void) 
{
  int texturewidth=0,textureheight=0;
  D3DX_SURFACEFORMAT texformat=D3DX_SF_UNKNOWN;
  if FAILED(D3DXCreateTextureFromFile(d3dd,
    NULL,
    (LPDWORD)&texturewidth,  
    (LPDWORD)&textureheight,
    &texformat,  
    NULL,
    &myTexture,  
    0,  
    "texmex.bmp",
    D3DX_FT_LINEAR)) return 0;
  return 1;
}

int APIENTRY WinMain(HINSTANCE hInstance,
                     HINSTANCE hPrevInstance,
                     LPSTR     lpCmdLine,
                     int       nCmdShow) 
{
  WNDCLASSEX winclass;
  HWND hWnd;
  MSG msg;

  int fs=0;
  if (MessageBox(NULL,"Shall we do it in fullscreen mode?",
                 "Silly question",MB_YESNO)==IDYES) fs=1;

  lpCmdLine=lpCmdLine;
  hPrevInstance=hPrevInstance;

  if (FAILED(D3DXInitialize())) return 0;

  mainhInst=hInstance;

  winclass.cbSize=sizeof(WNDCLASSEX);
  winclass.style=CS_DBLCLKS;
  winclass.lpfnWndProc=&WindowProc;
  winclass.cbClsExtra=0;
  winclass.cbWndExtra=0;
  winclass.hInstance=hInstance;
  winclass.hIcon=LoadIcon(NULL,IDI_APPLICATION);
  winclass.hCursor=LoadCursor(NULL,IDC_ARROW);
  winclass.hbrBackground=GetSysColorBrush(COLOR_APPWORKSPACE);
  winclass.lpszMenuName=NULL;
  winclass.lpszClassName=progname;
  winclass.hIconSm=NULL;

  if (!RegisterClassEx(&winclass))
    return 0;

  hWnd=CreateWindow(
    progname,
    progname,
    WS_SYSMENU|WS_CAPTION|WS_BORDER|WS_OVERLAPPED|WS_VISIBLE|WS_MINIMIZEBOX,
    CW_USEDEFAULT,
    0,
    640,
    480,
    NULL,
    NULL,
    hInstance,
    NULL);

  mainhWnd=hWnd;

  if (FAILED(D3DXCreateContext(  
              D3DX_DEFAULT,  
              D3DX_CONTEXT_FULLSCREEN*fs,  //windowed = 0
              hWnd,
              D3DX_DEFAULT,  
              D3DX_DEFAULT,  
              &dxctx))) return 0;
  d3dd=dxctx->GetD3DDevice();
  d3d=dxctx->GetD3D();

  d3dd->SetRenderState(D3DRENDERSTATE_CULLMODE,D3DCULL_NONE ); // no cull
  d3dd->SetRenderState(D3DRENDERSTATE_DITHERENABLE,TRUE); // dither on
  d3dd->SetRenderState(D3DRENDERSTATE_ZENABLE,D3DZB_FALSE); // no zbuf

  d3dd->SetRenderState(D3DRENDERSTATE_CLIPPING,FALSE); 
  d3dd->SetRenderState(D3DRENDERSTATE_LIGHTING,FALSE); 

  if (create_texture()==0) {
    d3d->Release();
    d3dd->Release();
    D3DXUninitialize();
    return 0;
  }
   
  ShowWindow(hWnd,nCmdShow);

  int frame=0;
  int starttime;
  starttime=GetTickCount();;
  char str[200];
  while (1) {
    reindeer();
    frame++;
    int sec=GetTickCount()-starttime;
    if (sec>0) sprintf(str,"frame:%05d sec:%05d.%03d fps:%3.3f (alt-F4 quits)",
                       frame,sec/1000,sec%1000,(frame*1000.0)/sec);
    dxctx->DrawDebugText((float)(2/640.0),(float)(2/480.0),0xffffff,str); 
      
    dxctx->UpdateFrame(0);

    if(PeekMessage(&msg,hWnd,0,0,PM_REMOVE)) {
      switch(msg.message) {
      case WM_QUIT:
      case WM_DESTROY:
        d3d->Release();
        d3dd->Release();
        D3DXUninitialize();
        return (msg.wParam);
      default: 
        DefWindowProc(hWnd,msg.message,msg.wParam,msg.lParam);
      }          
    }  
  }    
}

And off to the new stuff..

LPDIRECTDRAWSURFACE7 myTexture;

This is the surface for the texture.

buf[c].tu=(-sin((y+i*0.394)*0.12251474)+cos((x-i*0.202)*0.12654374));
buf[c].tv=(-cos((x+i*0.21)*0.12123474)+sin((-y+i*0.398)*0.12452474));

Here, in the setup-function we fill the U and V values of our vertices. Values range from 0 to 1; if they get over or under these values, the textures are wrapped. You can set the wrapping mode with D3DRENDERSTATE_WRAP0 render state. This may require a bit of an explanation. Let's say we have a wrapping texture around a cylinder. The place where the texture map both starts and ends requires wrapping flags to be on so that it doesn't draw the texture wrong way causing those nice "wrinkled" surfaces we've seen in far too many demos already.

d3dd->SetTexture(0,myTexture); 
d3dd->SetTexture(0,NULL);

The rendering function has these two new lines. First one sets our texture to the texture stage 0, and the last one un-assigns it. Direct3d supports 8 layers of textures with one call. Should you wish to render 8 different textures on top of each other you'd set the textures to stages 0 to 7. Multitexturing is, however, rather complex topic (and you'd need to know about blending modes etc before touching them), so we'll simply skip it.

int create_texture(void) 
{
  int texturewidth=0,textureheight=0;
  D3DX_SURFACEFORMAT texformat=D3DX_SF_UNKNOWN;
  if FAILED(D3DXCreateTextureFromFile(d3dd,
    NULL,
    (LPDWORD)&texturewidth,  
    (LPDWORD)&textureheight,
    &texformat,  
    NULL,
    &myTexture,  
    0,  
    "texmex.bmp",
    D3DX_FT_LINEAR)) return 0;
  return 1;
}

This new function calls D3DXCreateTextureFromFile to load our texture map from "texmex.bmp". D3dx also has calls that help in creating textures in memory, or just to find a suitable texture format from your 3d device. This, however, is the clearly easiest way (although I personally hate external files).

Parameters. First comes our d3d device, then pointer to flags (only "nomipmap" supported), then pointer to texture width and height (if zero, will be updated to the values from the image file). Then pointer to texture format which in our case is "unknown", or, we don't care, and d3d will figure out the texture format that matches the source file (in this case most probably 8r8g8b). Next one is pointer to palette, which we don't care about, pointer to our surface handle which will be created by this call, then number of mipmaps which we set to zero here, source filename and the filter type the call should use when generating mipmaps.

If it weren't for this call, we'd first figure out a bitmap format supported by the 3d card, created the surface, locked it, filled it (possibly via GDI buffer which is another nice mess) and generated mipmaps ourselves.

That's actually it. When you run the program you'll probably be disappointed by the frame rate (it's only 64*48=3072 polygons with single texture and gouraud shade). Unfortunately, there's nothing much to be done about it. The "millions of polys per sec" benchmarks refer to 6-pixel polygons drawn in strips, while we're rendering 50 pixel polys here. When you think about it, quakes and halflifes actually use rather few polys on screen at a time. Everything might change when transformation and lighting is done always in 3d hardware and fill rates have grown tenfold.. =)

I hope this has shed some light on d3d, and will help someone get things going.

Feel free to send comments to sol.