Sol's dirty DX7 tutorial

Polygons

Next we'll dump some polygons in. In this chapter we do it with normal array of vertices, and next chapter is devoted to wrapping the array into a vertexbuffer for various reasons.

#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

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

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

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

D3DTLVERTEX buf[(GRID_X+1)*(GRID_Y+1)]; // array of transformed & lit vertices
WORD idx[2*(GRID_X+1)*(GRID_Y+1)];      // array of indexes

void init(void) 
{
   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;
    }
}

void setup(void)
{ 
  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].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);
    }
}

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

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

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

  d3dd->BeginScene();

  for (int y=0;y<GRID_Y;y++)
    d3dd->DrawIndexedPrimitive(D3DPT_TRIANGLESTRIP,D3DFVF_TLVERTEX,
                               (void*)(buf),(GRID_X+1)*(GRID_Y+1),
                               (idx+y*(GRID_X+1)*2),(GRID_X+1)*2,0);

  d3dd->EndScene();
}

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

  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);
      }          
    }  
  }    
}

As before, we'll go through the source top-down, ignoring stuff you should already know about.

D3DTLVERTEX buf[(GRID_X+1)*(GRID_Y+1)]; // array of transformed & lit vertices
WORD idx[2*(GRID_X+1)*(GRID_Y+1)];      // array of indexes

Direct3d can handle all sorts of vertices, but there are three main types; unlit and untransformed, lit and untransformed and finally lit and transformed vertices. Normally we'd start with lit (or unlit) untransformed vertices and let d3d do the transformations (and maybe lighting) for us. However, in this tutorial we don't care about that and just use the final screen-coordinate vertices.

The array of indexes is an array of indexes to the vertex buffer. We'll come back to the array of indexes shortly.

The init function fills the index buffer and vertex buffer with our values. This is only done once.

The setup function merely renders some nice rgb values to the vertices, which is done per frame (this is the only real "effect" in this program).

Now the most interesting changes have happened in the reindeer-function:

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

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

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

  d3dd->BeginScene();

  for (int y=0;y<GRID_Y;y++)
    d3dd->DrawIndexedPrimitive(D3DPT_TRIANGLESTRIP,D3DFVF_TLVERTEX,
                               (void*)(buf),(GRID_X+1)*(GRID_Y+1),
                               (idx+y*(GRID_X+1)*2),(GRID_X+1)*2,0);

  d3dd->EndScene();
}

(Before anyone mails me and says that it's "render" not "reindeer", yes, I'm aware of the fact).

First we check if init has been called and call the init if it has not. Then we call setup, which fills in those nice rgb values. We have commented the clear call out since we don't use zbuffer and we fill the whole screen every time.

The real rendering part is actually just that one DrawIndexedPrimitive call. The SDK doc says:

HRESULT DrawIndexedPrimitive(
  D3DPRIMITIVETYPE d3dptPrimitiveType,  
  DWORD  dwVertexTypeDesc,              
  LPVOID lpvVertices,                   
  DWORD  dwVertexCount,                 
  LPWORD lpwIndices,                    
  DWORD  dwIndexCount,                  
  DWORD  dwFlags                        
);

Primitive type can be any of the following: point list, line list, line strip, triangle list, triangle strip or triangle fan. What do they mean?

For point list, every vertex in the list (or in the case of this call, for every index to the vertex buffer in the index buffer) means one point rendered. For line list, every two vertices means one line. For line strip, every vertex after the first one means one line (i.e.. 3 vertices means 2 lines; 4 vertices means 3 lines etc, from vertex 1 to 2, 2 to 3, 3 to 4).

For triangle list, every three vertices means one three-sided polygon rendered. This is most probably the one primitive type you'll use the most. Triangle strip, as used in this example, means one triangle per vertex after the original two, formatted like:

1---3---5   7
|  /|  /
| / | / 
|/  |/
2---4   6   8

In here the first poly would be 1-2-3, second 2-3-4, third 3-4-5 etc. Triangle fans can be used to render N-sided convex polygons, and work in much the same way, except that the first vertex stays the same (1-2-3, 1-3-4, 1-4-5 etc), like this:

3---4---5
|\  |  /
| \ | /
|  \|/
2---1   6

Now that you know how the triangle strip works you should be able to figure out how init and setup functions work, and why. If you can't figure them out, you're quite likely outside the target audience of this document anyway =), but don't worry, you probably will not be using triangle strips too much in any case.

If you've seen Traumatique (asm99 2nd place), I used triangle strip in the electricity effect in the beginning, triangle lists for most of the stuff and triangle fans for sprites (as any of the square sprites could be clipped to be up to 8-sided).

Let's get back to the DrawIndexedPrimitive call. VertexTypeDesc naturally describes the type of the vertex (the call can take nontransformed vertices as well, and will perform any transformations you have defined (which we will not be covering in this tutorial at all) which is nifty feature but not too useful as I see it - you should do transformations before you get to drawing stuff). The next parameters are pointers to vertex buffers and index buffers and counts of the data in each. You can set the flags parameter to D3DDP_WAIT to wait until all polys have been sent to the card. This does not mean that they'd actually get rendered before the call returns.

Apart from DrawIndexedPrimitive d3d also supports DrawPrimitive call family. So why use indexed calls? There are three reasons; First, less work done. Especially when you let the d3d do the transformations for you, you'd just be recalculating vertices of neighboring polygons without reason. Second, bandwidth. 3d card drivers should be able to optimize the data thrown to the card. Third, gaps. Even if you use the same vertex data in two calls, some cards manage to render gaps between polygons if you don't use the indexed call system.

Yes, I said call family. The primitive draw calls can be to normal vertex arrays, strided vertices or vertexbuffer objects. We'll ignore strided vertices here (as they can't be used with screen space only) and move on to vertex buffers..