email sol follow sol rss feed of the blog wishlist Sol::Tutorials

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;yDrawIndexedPrimitive(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.

Site design & Copyright © 2021 Jari Komppa
Possibly modified around: June 01 2010