English Welcome Kecy Programování 3D - Engine Guestbook Odkazy Downloady O autorovi Napiš mi Mailform
box_cz 3D - Engine 02 - matice a transformace box_cz

Tak už jsem tady zase ! Dneska si ještě vysvětlíme ty matice a pak už se vrhneme na první 3D Engine. Jo když jsem si na to vzpomněl - trochu to komentujte lidi ! Copak nikdo nemáte ruce, abyste mohli napsat e-mail ? Vždyť ani nevím jestli to vůbec někdo čte, jestli je všechno jasný, jestli jdu dost pomalu a srozumitelně. Trošku se taky snažte, vy ! Přece není možný aby to každej chápal. No nic, snad jsem vás trochu povzbudil ;-) Tak do těch matic : Matice je matematické vyjádření pole čísel o dvou souřadnicích (přesně je to pole vektorů, kde vektor je pole čísel o nějaké velikosti) :

matice

Taková matice by se pak značila anm (všechny písmena za a jsou indexy). Pro 3d engine budeme používat matice o rozměrech 4×4. Je nutné aby byly hodně přesné a proto budou v c-čku realizované typem float :


typedef float Matrix[4][4];

Co pak ale s maticí ? Co vlastně matice obsahuje ? Jsou to čtyři vektory. Jeden ukazuje kde bude "vpravo", druhý ukazuje "nahoře" a třetí míří "dopředu". Ten čtvrtý je vlastně bod, je to posunutí od počátku souřadnicového systému (značí se O, O = [0, 0, 0]) Jsou umístěny svisle v prvních třech řádcích matice. Z toho plyne že transformace bodu maticí by měla vypadat nějak takhle :


tx = x * m[0][0] + y * m[1][0] + z * m[2][0] + m[3][0]
ty = x * m[0][1] + y * m[1][1] + z * m[2][1] + m[3][1]
tz = x * m[0][2] + y * m[1][2] + z * m[2][2] + m[3][2]

Tady jsou ale využité jen tři řádky matice. Co s tím čtvrtým ? Jestli jste se dívali do nějakých papírů o maticích tak jste si asi všimli že když chcete mezi sebou násobit matice, musí jedna mít stejný počet řádků jako druhá počet sloupců. Jsou sice systémy, které uchovávají v paměti jen ty tři řádky a čtvrtý se doplňuje až při použití matice, nebo je matice tři na tři a jeden bod pro pozici a šetří se tím paměť. Tím to ale nebudeme komplikovat, hlavně když se dnes prodávají 128 Mb moduly za pár stovek. Teď ale přichází otázka, co v paměti má být. Z transformace můžete lehce odvodit, že aby byl objekt v původní poloze (tx = x, ty = y, tz = z) musí být matice jednotková. Taková matice má jedničky jen v tzv. hlavní diagonále :

jednotková matice

No a s tímto tvarem matice přichází i první funkce :


void Matrix_Init(Matrix *m)
{
    int i, j;

    for(i = 0; i < 4; i ++)
        for(j = 0; j < 4; j ++)
            (*m)[i][j] = (float)(i == j);
}

Možná si říkáte co to je za zápis, ale je to zcela normální céčkovský zápis, který urychlí vaši práci :-). Dobře, tak máme matici na začátku. Ale co když budu chtít s objektem někam pohnout ? Některé z vás možná napadlo že poslední sloupec matice (m[3][0], m[3][1], m[3][2]) budou posunutí po osách světa. Takže pokud chcete zjistit pozici objektu nebo kamery tak jen přečtete tyhle buňky. Z toho potom vychází i naše Backface culling. Ale už jsme si řekli, že objekt se bude posouvat po svých osách a ne po osách světa, takže posouvání se nedělá přičítáním čísel do matice (i když by to samozřejmě fungovalo), ale vytvoří se nová matice, inicializuje se a místo těchto tří buněk se dají hodnoty posunu po osách objektu (kamery). A naše milá matice objektu se s touto maticí vynásobí a je hotovo. Takže budeme potřebovat ještě násobení matic. Když to děláte na papír, sepíšete si první matici pod a vlevo od druhé a násobíte řádky se sloupci a výsledky potom sčítáte. (takže a × b nerovná se b × a !) V céčku to bude vypadat nějak takhle :


void Matrix_Multiply(Matrix *a, Matrix *b, Matrix *r)
{
    Matrix temp;
    int i, j;

    for(i = 0; i < 4; i ++) {
        for(j = 0; j < 4; j ++)
            temp[i][j] = (*a)[0][j] * (*b)[i][0] + (*a)[1][j] * (*b)[i][1] +
                (*a)[2][j] * (*b)[i][2] + (*a)[3][j] * (*b)[i][3];
    }
    memcpy(r, temp, sizeof(Matrix));
}

Je to jednoduché a prosté. Nebo ne ? Můžete si to porovnat s vaší učebnicí matematiky, pokud nějakou máte. No a už si můžeme napsat funkci pro posunutí matice :


void Matrix_Translate(Matrix * m, float dx, float dy, float dz)
{
    Matrix temp;

    Matrix_Init(&temp);

    temp[3][0] = dx;
    temp[3][1] = dy;
    temp[3][2] = dz;

    Matrix_Multiply(m, &temp, m);
}

Je to přesně jak jsme si to řekli. Vytvoří se jednotková, dají se koeficienty posunutí a vynásobí se s maticí objektu. Úplně stejné to je s rotací. Můžete si to odvodit z rotace bodu okolo nuly o úhel. Když rotujete podle osy X, tak vlastně budete měnit jen Y a Z vektory (Y je nahoru a Z bude dopředu) Já už jenom vypíšu zdrojáky, stejně je to už dneska docela dlouhý :


void Matrix_Rotate_X(Matrix *m, float angle)
{
    Matrix temp;

    Matrix_Init(&temp);

    temp[2][1] = (float)-sin(angle);
    temp[2][2] = (float)cos(angle);
    temp[1][1] = (float)cos(angle);
    temp[1][2] = (float)sin(angle);

    Matrix_Multiply(m, &temp, m);
}

void Matrix_Rotate_Y(Matrix *m, float angle)
{
    Matrix temp;

    Matrix_Init(&temp);

    temp[0][0] = (float)cos(angle);
    temp[0][2] = (float)-sin(angle);
    temp[2][2] = (float)cos(angle);
    temp[2][0] = (float)sin(angle);

    Matrix_Multiply(m, &temp, m);
}

void Matrix_Rotate_Z(Matrix *m, float angle)
{
    Matrix temp;

    Matrix_Init(&temp);

    temp[0][0] = (float)cos(angle);
    temp[0][1] = (float)sin(angle);
    temp[1][1] = (float)cos(angle);
    temp[1][0] = (float)-sin(angle);

    Matrix_Multiply(m, &temp, m);
}

No, a máme to za sebou. Teď už je jen jedna otázka. Umíme pohybovat s objekty, kamerou, ale pořád ještě nevíme jak smíchat matici objektu a kamery tak, aby se s ní daly transformovat vertexy. Dělá se to tak, že vezmete matici kamery, vytvoří se k ní inverzní matice a k ní ještě transponovaná matice (prohozené řádky a sloupce - obvykle se to nekomplikuje a dá se to všechno do jedné funkce, počítající inverzní matici) a tou se vynásobí matice objektu. Výsledná matice je tzv. transformační matice a tou se transformují vertexy tak jak jsme si řekli na začátku. K tomu potřebujeme nějakou funkci, která by inverzní matici spočítala. Inverzní matice se docela jednoduše počítá pomocí determinantu. Vezme se determinant celé matice (nesmí vyjít nula) a potom subdeterminanty. Ty se počítají pro každý prvek nové matice. Je to vlastně determinant té první, ale vynechá se řádek a sloupec, v němž leží onen prvek a celý se vynásobí mínus jedničkou, umocněnou na součet indexů toho prvku. Nakonec se všechny prvky vydělí společným determinantem. My ale víme že chceme v posledním řádku matice "0 0 0 1", a tak se to dá trochu zoptimalizovat. Ještě pro pořádek bych měl říct že součinem matice a matice k ní inverzní je jednotková matice. Tady to je :


void Matrix_Inverse(Matrix *src, Matrix *inv)
{
    float f_det;
    int i, j;

    f_det = (*src)[0][0] * (*src)[1][1] * (*src)[2][2] +
            (*src)[1][0] * (*src)[2][1] * (*src)[0][2] +
            (*src)[0][1] * (*src)[1][2] * (*src)[2][0] -
            (*src)[0][2] * (*src)[1][1] * (*src)[2][0] -
            (*src)[0][1] * (*src)[1][0] * (*src)[2][2] -
            (*src)[0][0] * (*src)[1][2] * (*src)[2][1];
    // spocita determinant podle sarrusova pravidla
    // u matice se spodkem "0 0 0 1" je determinant
    // stejny jako posledni doplnek

    (*inv)[0][0] = (*src)[1][1] * (*src)[2][2] /** (*src)[3][3] +*/ // 33 = 1
                   /*(*src)[2][1] * (*src)[3][2] * (*src)[1][3] +*/ // 13 = 0
                   /*(*src)[1][2] * (*src)[2][3] * (*src)[3][1] -*/ // 23 = 0
                   /*(*src)[3][1] * (*src)[2][2] * (*src)[1][3] -*/ // 13 = 0
                   /*(*src)[1][1] * (*src)[2][3] * (*src)[3][2] -*/ // 23 = 0
                   -(*src)[1][2] * (*src)[2][1] /** (*src)[3][3]*/; // 33 = 1
    (*inv)[1][0] =-(*src)[1][0] * (*src)[2][2] /** (*src)[3][3] +*/ // 33 = 1
                   /*(*src)[2][0] * (*src)[3][2] * (*src)[1][3] +*/ // 13 = 0
                   /*(*src)[1][2] * (*src)[2][3] * (*src)[3][0] -*/ // 23 = 0
                   /*(*src)[1][3] * (*src)[2][2] * (*src)[3][0] -*/ // 13 = 0
                   /*(*src)[2][3] * (*src)[3][2] * (*src)[1][0] -*/ // 23 = 0
                   +(*src)[1][2] * (*src)[2][0] /** (*src)[3][3]*/; // 33 = 1
    (*inv)[2][0] = (*src)[1][0] * (*src)[2][1] /** (*src)[3][3] +*/ // 33 = 1
                   /*(*src)[1][3] * (*src)[2][0] * (*src)[3][1] +*/ // 13 = 0
                   /*(*src)[2][3] * (*src)[1][1] * (*src)[3][1] -*/ // 23 = 0
                   /*(*src)[1][3] * (*src)[2][1] * (*src)[3][0] -*/ // 13 = 0
                   /*(*src)[2][3] * (*src)[3][2] * (*src)[1][0] -*/ // 23 = 0
                   -(*src)[1][1] * (*src)[2][0] /** (*src)[3][3]*/; // 33 = 1
    (*inv)[3][0] =-(*src)[1][0] * (*src)[2][1] * (*src)[3][2] -
                   (*src)[1][1] * (*src)[2][2] * (*src)[3][0] -
                   (*src)[2][0] * (*src)[3][1] * (*src)[1][2] +
                   (*src)[1][2] * (*src)[2][1] * (*src)[3][0] +
                   (*src)[1][1] * (*src)[2][0] * (*src)[3][2] +
                   (*src)[2][2] * (*src)[1][0] * (*src)[3][1];
    // takhle moc se to všecko zkrátí, dál už to nebudu rozepisovat ...

    (*inv)[0][1] =-(*src)[0][1] * (*src)[2][2] + (*src)[0][2] * (*src)[2][1];
    (*inv)[1][1] = (*src)[0][0] * (*src)[2][2] - (*src)[0][2] * (*src)[2][0];
    (*inv)[2][1] =-(*src)[0][0] * (*src)[2][1] + (*src)[0][1] * (*src)[2][0];
    (*inv)[3][1] = (*src)[0][0] * (*src)[2][1] * (*src)[3][2] +
                   (*src)[0][1] * (*src)[2][2] * (*src)[3][0] +
                   (*src)[2][0] * (*src)[3][1] * (*src)[0][2] -
                   (*src)[0][2] * (*src)[2][1] * (*src)[3][0] -
                   (*src)[0][1] * (*src)[2][0] * (*src)[3][2] -
                   (*src)[2][2] * (*src)[0][0] * (*src)[3][1];
    (*inv)[0][2] = (*src)[0][1] * (*src)[1][2] - (*src)[0][2] * (*src)[1][1];
    (*inv)[1][2] =-(*src)[0][0] * (*src)[1][2] + (*src)[0][2] * (*src)[1][0];
    (*inv)[2][2] = (*src)[0][0] * (*src)[1][1] - (*src)[0][1] * (*src)[1][0];
    (*inv)[3][2] =-(*src)[0][0] * (*src)[1][1] * (*src)[3][2] -
                   (*src)[0][1] * (*src)[1][2] * (*src)[3][0] -
                   (*src)[1][0] * (*src)[3][1] * (*src)[0][2] +
                   (*src)[0][2] * (*src)[1][1] * (*src)[3][0] +
                   (*src)[0][1] * (*src)[1][0] * (*src)[3][2] +
                   (*src)[1][2] * (*src)[0][0] * (*src)[3][1];

    for(j = 0; j < 4; j ++)
        for(i = 0; i < 3; i ++)
            (*inv)[j][i] /= f_det;
    // všecko vydělí determinantem (kromě posledního sloupce)

    (*inv)[0][3] = 0;
    (*inv)[1][3] = 0;
    (*inv)[2][3] = 0;
    (*inv)[3][3] = 1;
    // při matici se spodním řádkem "0 0 0 1"
    // vyjde vždycky poslední sloupec takhle.
}

Tak to by bylo. Pro pořádek - Sarrusovo pravidlo je pravidlo pro počítání determinantu. V matici se násobí šikmo jdoucí sloupce a ty se sčítají / odčítají podle směru. Kdo by chtěl vidět jak se to dělá postupně, je to v sekci Javascript na Techiho stránkách Ale jestli si vzpomínáte, řekl jsem že souřadnice se musí ještě perspektivně korigovat. K tomu potřebujete konstantu perspektivního zkreslení, pro rozlišení 320×200 je to pro úhel 45° zhruba 256. Dá se to odvodit z podobnosti trojúhelníků - kamera totiž vidí "pyramidu" a vy potřebujete obraz předělat tak, aby se ta pyramida vešla do "kvádru", jehož přední stranou je vaše obrazovka. To znamená že souřadnice budete dělit vzdáleností a násobit právě tímhle číslem. Ve výsledku to je : z_delta = n_Width / (tan(psi / 2) * 2); Kde n_Width je šířka obrazu a psi je zorný úhel (tady v radiánech). Normálně je to 90°, ale jsou i jiné možnosti použití - "aliení vidění" je něco okolo 150° a sniper - mód bývá tak 20° (ve fyzice jste se možná učili že dalekohled, mikroskop .. pracují právě se zorným úhlem oka ;-)) Čím míň vyjde z_delta, tím větší úhel. Nula by pak byla 180 stupňů, ale tu tam dát nesmíte, jedině číslo blízké nule. Už to rovnou hodím do céčka :


struct Vertex {
    float x, y, z;
};

struct Object{
    int n_vertex_num;
    Vertex *vertex;
    Matrix m_object;
};

void Draw_Object(Object *p_object, Matrix *p_camera)
{
    float tx, ty, tz;
    float x, y, z;
    MATRIX m;
    int i;

    invertmatrix(p_camera, &m);
    matmult(&m, &p_object->m_object, &m);
    // vytvoří se transformační matice tak jak jsme si říkali

    for(i = 0; i < p_object->n_vertex_num; i ++) {
        x = p_object->vertex[i].x;
        y = p_object->vertex[i].y;
        z = p_object->vertex[i].z;

        tx = x * m[0][0] + y * m[1][0] + z * m[2][0] + m[3][0];
        ty = x * m[0][1] + y * m[1][1] + z * m[2][1] + m[3][1];
        tz = x * m[0][2] + y * m[1][2] + z * m[2][2] + m[3][2];
        // transformují se jednotlivé vertexy

        if(tz > 0.1) { // pokud je vertex před kamerou
            tx = (320 / 2) + (tx * z_delta) / tz;
            ty = (200 / 2) + (ty * z_delta) / tz;
            // perspektivní korekce

            if(tx > 0 && tx < 320 && ty > 0 && ty < 200) {
                Draw_Point(tx, ty);
            } // vykreslení bodu na obrazovku
        }
    }
}

No a je to ! Teď už můžete napsat jenoduchý engine, který by zobrazoval body nějákého objektu. Potřebujete jen nějaké grafické rozhraní, pro začátek stačí jen Windowsovské okno. Taky potřebujete něco, odkud byste zdrojáky tahali - dneska použijeme formát ASC ze 3D - Studia (myslím že ho umí i milkshape, ke stažení někde na swedishcake.com ..) To je docela dobrej formát, protože se dá normálně otevřít v notepadu - jako text. Všechno je zapsaný jako text, takže není složitý z toho vytáhnout objekty a jejich verterxy. Později (asi v příštím díle) vám ukážu jak se to dělá s DirectX. Taky místo ASC tu bude 3ds (3D - Studio) formát. Tak si ho někde sežeňte (3D - Studio nebo 3D MAX). No a teď už si můžete stáhnout jednoduchý sampl i se zdrojáky :

engine 01

 3D Engine 01



zpět


Valid HTML 4.01!
Valid HTML 4.01