Sol's dirty DX7 tutorial

Polygone

Als n�chstes werfen wir ein paar Polygone ein. In diesem Kapitel tun wir das mit normalen Vertex Arrays und das n�chste Kapitel behandelt dann diese Arrays in einen Vertex Buffer zu legen.

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

Wie zuvor auch gehen wir von oben nach unten durch den Code, ignorieren aber die Teile, die ihr schon kennen solltet.

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 kann mit allen m�glichen Arten von Vertices umgehen, aber es gibt 3 Haupttypen: unbeleuchtet und nicht gewandelt, beleuchtet und nicht gewandelt und schlie�lich beleuchtet und umgewandelt. Normalerweise f�ngt man mit beleuchteten (oder unebleuchteten) Vertices an und l��t D3D die Transformationen (und die Beleuchtung) durchf�hren. In diesem Tutorial k�mmert uns das allerdings nicht und wir nutzen nur die endg�ltigen Bildschirmkoordinaten.

Der Array mit Indexen ist ein Array mit Indexen zum Vertex buffer. Wir kommen auf diese Arrays gleich noch einmal zur�ck.

Die init Funktion f�llt den Index Buffer und den Vertex Buffer mit unseren Werten. Dies isr nur einmal der Fall.

Die Setup Funktion rendert einige nette RGB Werte in die Vertices, was jeden Frame gemacht wird (das ist der einzig wirkliche "Effekt" in diesem Programm).

Nun die interessanteste �nderung, die in der reindeer-Funktion passiert:

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

(Bevor jemand mailt, das es "render" hei�t und nicht "reindeer", ja, das ist mir bekannt).

Als erstes �berpr�fen wir ob die Init bereits aufgerufen wurde oder nicht. Dann rufen wir das Setup auf, welches die netten RGB Werte einsetzt. Wir haben den L�schungsaufruf auskommentiert, da wir keinen Z-Buffer haben und jedesmal so wie so den ganzen Bildschirm f�llen.

Der wirkliche Render-Teil ist in Wirklichkeit nur der eine DrawIndexPrimitive Aufruf. Das SDK Doc sagt:

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

Ein Primitve Typ kann eines der folgenden sein: Point List, Line List, Line Strip, Triangle List, Triangle Strip oder Triangle Fan. Was bedeutet das?

F�r eine Point List bedeutet jeder Vertex in der Liste (oder im Falle dieses Aufrufes jeder Index zum Vertex Buffer im Index Buffer) einen gerenderten Punkt. F�r eine Line List bedeuten immer zwei Vertices eine Linie. F�r Line Strip bedeutet jeder Vertex nach dem ersten eine Linie (z.B. 3 Vertices bedeuten 2 Linien, 4 Vertices bedeuten 3 Linien, usw. von Vertex 1 zu 2, 2 zu 3, 3 zu 4).

F�r eine Triangle List bedeuten je 3 Vertices ein dreiseitig dargestelltes Polygon. Das ist vermutlich der am mei�ten benutzte Primitive Typ. Triangle Strip, so wie wir es in diesem Teil verwenden, bedeutet ein Dreieck pro Vertex nach den ersten zwei, so formatiert:

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

In diesem Fall w�re das erste Poly 1-2-3, das zweite 2-3-4, das dritte 3-4-5 usw. Triangle Fans k�nnen benutzt werden um n-Seitige, konvexe Polygone zu rendern. Sie funktionieren genauso, au�er das der erste Vertex der gleiche bleibt (1-2-3, 1-3-4, 1-4-5 usw:), etwa so:

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

Nun da ihr wisst wie Triangle Strips funktionieren solltet ihr in der Lage sein herauszufinden wie die Init und Setup Funktionen arbeiten. Wenn ihr es nicht k�nnt seid ihr so wie so klar au�erhalb der Zielgruppe dieses Textes =), aber keine Sorge, ihr werdet Triangle Strips eh selten benutzen.

Wenn ihr Traumatique (2. Platz, asm99) gesehen habt, dort habe ich Triangle Strips am Anfang f�r den Elektrizit�ts-Effekt genutzt, Triangle Lists f�r den mei�ten anderen Kram und Triangle Fans f�r Sprites (da jeder der quadratischen Sprites bis zu 8-seitig sein konnte).

Zur�ck zu dem DrawIndexedPrimitive Aufruf. VertexTypeDesc beschreibt nat�rlich die Art des Vertex (der Aufruf kann auch nicht transformierte Vertices verwalten und wird jede Form von Transformation, die ihr angegeben habt ausf�hren (was wir aber �berhaupt nicht in diesem Tutorial behandeln), was ein nettes Feature ist, aber nicht sehr sinnvoll so wie ich das sehe - ihr solltet eure Transformationen immer durchf�hren bevor ihr etwas darstellt). Die n�chsten Parameter sind Pointer zu Vertex Buffern und Index Buffern und geht alle Daten in jedem durch. Ihr k�nnt den Parameter D3DDP_WAIT setzen und so warten, bis alle Polys zur Karte gesendet wurden. Das bedeutet aber nicht das sie wirklich dargestellt werden bevor der Aufruf sich zur�ck meldet.

Abgesehen von DrawIndexedPrimitive unterst�tzt D3D auch die DrawPrimitive Aufruf-Familie. Warum nutzen wir also indexierte Aufrufe? Aus drei Gr�nden: Erstens, weniger Arbeit. Besonders wenn man D3D die Transformationen �bernehmen l�sst werdet ihr ohne Grund noch die Vertices der benachbarten Polygone mitberechnen. Zweites, Bandbreite. Die Treiber der 3D Karte sollten in der Lage sein die Daten zu optimieren, die man ihnen hinwirft. Drittens, L�cken. Selbst wenn man die selben Vertex Daten in zwei Aufrufen nutzt schaffen es manche Karten L�cken zu produzieren, es sei denn man nutzt indexierte Aufrufe.

Ja, ich sagte Aufruf-Familie. Die primitive draw Aufrufe k�nnen normale Vertex Arrays sein, strided Vertices oder Vertex Buffer-Objekte. Wir ignorieren hier mal die strided Vertices (da sie nicht mit normalen Bildschirmgr��en genutzt werden k�nnen) und machen weiter mit Vertex Buffern.