|
3D - Engine 04 - klipovací pyramida a kreslení polygonů
Tak jsem tu a se mnou i čtvrtý díl našeho seriálu. Pokud někdo nezačne pořádně rychle
projevovat zájem o seriál, tak to balím. Chápu že o tom moc lidí asi neví a tak když
dá mailem někdo vědět, že chce, abych pokračoval tak zase budu. Ale doteď mi napsal
jen jeden človíček ohledně céčka ! Takže pište, parchantíci :-) No, když už to začínalo
být zajímavé, tak končím. Dneska jsem vám chtěl vysvětlit minule pouze okrajově
vzpomínanou klipovací pyramidu a s ní i kreslení polygonů. Takže první skutečný engine a
vy to nečtete ! Takže jak jsem říkal už minule, klipovací pyramida je čtveřice rovin o
kterou se polygony "ořezávají", aby byly pouze v prostoru obrazovky. Obecnou rovinu
vytvoříte ze tří bodů takhle :
struct Plane {
float a, b, c, d;
};
struct Vector {
float x, y, z;
};
#define _Len(A) ((float )sqrt(A.x * A.x + A.y * A.y + A.z * A.z))
#define _Normalize(A) {float s = _Len(A); A.x /= s; A.y /= s; A.z /= s; }
#define _Dot(A,B) (B.x * A.x + A.y * B.y)
#define _Cross(A,B,C) C.x = B.y * A.z - A.y * B.z; \
C.y = B.z * A.x - A.z * B.x; \
C.z = B.x * A.y - A.x * B.y;
Vector u, v, w;
u.x = c.x - a.x;
u.y = c.y - a.y;
u.z = c.z - a.z;
v.x = b.x - a.x;
v.y = b.y - a.y;
v.z = b.z - a.z;
_Cross(u, v, w);
_Normalize(w);
p_plane->a = w.x;
p_plane->b = w.y;
p_plane->c = w.z;
p_plane->d = -(p_plane->a * a1 + p_plane->b * a2 + p_plane->c * a3);
|
Tak :-) a co s tím ? Plane je rovina, definovaná normálovým vektorem (a, b, c) a kolmou
vzdáleností od počátku souřadnicového systému (d). Vector je vektor se souřadnicemi.
_Len spočítá délku vektoru. _Normalize upraví vektor tak, aby jeho délka byla jedna.
No a _Cross spočítá tzv. cross-product, tj. do w se zapíše vektor, kolmý na vektor u
a zároveň na vektor v. Vektory u a v jsou dva vektory, ležící v rovině, definované třemi
body a, b a c. Teď už by snad mohlo být jasno. Tak zpátky k pyramidě. Její definice bude
vypadat nějak takhle :
struct Plane {
float a, b, c, d;
};
struct Pyramid {
Plane p0, p1, p2, p3;
};
Pyramid pyramid;
|
Mohl bych ještě říct že se rovina dá transformovat maticí (jako snad všechno ;-)) Takže vidíme,
že pyramida je opravdu ze čtyř rovin. Ale jak je spočítat ? V praxi se zadá rozlišení
a perspektivní zkreslení, které musí být stejné jako při výpočtu 2d souřadnic bodu. A vypadá
to asi takhle :
void SetFOV(float n_Width, float n_Height, float f_z_delta, Pyramid *p_pyramid)
{
float x_right, y_bottom, x_left, y_top;
x_left = -n_Width / (2 * f_z_delta);
y_top = n_Height / (2 * f_z_delta);
x_right = n_Width / (2 * f_z_delta);
y_bottom = -n_Height / (2 * f_z_delta);
SetPlane(&p_pyramid->p0, 0, 0, 0, x_right, 0, 1, x_right, 1, 1);
SetPlane(&p_pyramid->p1, 0, 0, 0, x_left, 1, 1, x_left, 0, 1);
SetPlane(&p_pyramid->p2, 0, 0, 0, 0, y_bottom, 1, 1, y_bottom, 1);
SetPlane(&p_pyramid->p3, 0, 0, 0, 1, y_top, 1, 0, y_top, 1);
}
|
Tak. Parametry n_Width a n_Height je výška a šířka obrazovky, f_z_delta je ona konstanta
perspektivního zkreslení (měly by být dvě, jedna pro horizontální a druhá pro vertikální
rozměr (obrazovka není čtvercová), ale my používáme jen jednu). Pro 320×200 je to přesně
320 pro 45°, už jsem to jednou říkal, i jak to spočítat pro jiný úhel. p_pyramid je klipovací
pyramida, která se zrovna počítá. Tak, pyramidu už máme vygenerovanou. Ale co teď s ní ?
Pro zjednodušení napíšeme ještě dvě funkce, které by neměl být problém pochopit.
enum {
_Front,
_Back,
_Onplane,
_Split
};
int inline ClassifyVtxPl(Vertex *p_vertex, Plane *p_plane)
{
float p;
p = p_plane->a * p_vertex->x + p_plane->b * p_vertex->y +
p_plane->c * p_vertex->z + p_plane->d;
if (p > 0)
return _Front;
if (p < 0)
return _Back;
return _Onplane;
}
int inline ClassifyFacePl(Face *p_face, Plane *p_plane)
{
int fn = 0, bn = 0, i;
for (i = 0; i < 3; i ++){
switch (ClassifyVtxPl(p_face->vertex[i], p_plane)){
case _Front:
fn ++;
break ;
case _Back:
bn ++;
break ;
case _Onplane:
fn ++;
bn ++;
break ;
}
}
if (fn == 3)
return _Front;
if (bn == 3)
return _Back;
return _Split;
}
|
Tak. Teď už zjistíme, jestli face leží před, za nebo je protínán rovinou. Ale ještě ho
neumíme rozdělit. Face se dělí po jednotlivých hranách (úsečkách), které ho tvoří. Jak
získat průsečík přímky, definované dvěma body s rovinou popisuje následující funkce.
Vertex IntersectSegmentPl(Vertex *p_v1, Vertex *p_v2, Plane *p_plane)
{
float a, b, t;
Vertex vertex;
a = p_plane->a * p_v1->x + p_plane->b * p_v1->y +
p_plane->c * p_v1->z + p_plane->d;
b = p_plane->a * p_v2->x + p_plane->b * p_v2->y +
p_plane->c * p_v2->z + p_plane->d;
if (b - a == 0)
return *p_v1;
t = a / (b - a);
vertex.x = p_v1->x + t * (p_v1->x - p_v2->x);
vertex.y = p_v1->y + t * (p_v1->y - p_v2->y);
vertex.z = p_v1->z + t * (p_v1->z - p_v2->z);
vertex.u = p_v1->u + t * (p_v1->u - p_v2->u);
vertex.v = p_v1->v + t * (p_v1->v - p_v2->v);
return vertex;
}
|
Tak a máme to skoro všechno. Doufám, že jste to z toho pochopili. Kdyby ne, napište a já
to budu víc rozepisovat :-) Teď už jen zbývá stvořit funkci, která rozdělí face rovinou.
My z něj budeme potřebovat vždy jen jednu stranu (tu uvnitř pyramidy), takže to bude
jednodušší. Vezmeme vždy jednu hranu ( edge) a testujeme její vrcholy. Pokud leží oba na
jedné straně, hrana se buď použije celá ( _Front), nebo se celá zahodí ( _Back). Pokud ne,
hrana se rozdělí a přidá se nový vrchol. Je to docela jednoduché :
struct Polygon {
int n_vertex_num;
Vertex vertex[7];
};
Polygon SplitPolygon(Polygon *p_poly, Plane *p_plane)
{
int i, prev, status[7];
Polygon tmp;
tmp.n_vertex_num = 0;
for (i = 0; i < p_poly->n_vertex_num; i ++)
status[i] = ClassifyVtxPl(&p_poly->vertex[i], p_plane);
prev = p_poly->n_vertex_num - 1;
for (i = 0; i < p_poly->n_vertex_num; i ++) {
if (status[i] == _Front) {
if (status[prev] == _Back) {
tmp.vertex[tmp.n_vertex_num] =
IntersectSegmentPl(&p_poly->vertex[prev],
&p_poly->vertex[i], p_plane);
tmp.n_vertex_num ++;
}
tmp.vertex[tmp.n_vertex_num] = p_poly->vertex[i];
tmp.n_vertex_num ++;
} else if (status[prev] == _Front) {
tmp.vertex[tmp.n_vertex_num] =
IntersectSegmentPl(&p_poly->vertex[prev],
&p_poly->vertex[i], p_plane);
tmp.n_vertex_num ++;
}
prev = i;
}
return tmp;
}
|
No, ani to nebolelo. Teď už se to složí všechno jen do těla jedné funkce a zábava může
začít ! Ona svatá funkce, která ušetří spoustu času při počítání polygonů je zde :
Polygon Clip_Face(Face *p_face, Pyramid *p_pyramid)
{
int a, b, c, d, i;
Polygon poly;
a = ClassifyFacePl(p_face, &p_pyramid->p0);
b = ClassifyFacePl(p_face, &p_pyramid->p1);
c = ClassifyFacePl(p_face, &p_pyramid->p2);
d = ClassifyFacePl(p_face, &p_pyramid->p3);
poly.n_vertex_num = 3;
for (i = 0; i < 3; i ++)
poly.vertex[i] = p_face->vertex[i][0];
if (a == _Front && b == _Front && c == _Front && d == _Front)
return poly;
if (a == _Back || b == _Back || c == _Back || d == _Back) {
poly.n_vertex_num = 0;
return poly;
}
if (a == _Split)
poly = SplitPolygon(&poly, &p_pyramid->p0);
if (b == _Split)
poly = SplitPolygon(&poly, &p_pyramid->p1);
if (c == _Split)
poly = SplitPolygon(&poly, &p_pyramid->p2);
if (d == _Split)
poly = SplitPolygon(&poly, &p_pyramid->p3);
return poly;
}
|
To by jsme měli. Když to přebereme, bude naše stará - ne ta stará ! je vidět na co myslíte,
hovada :-) - DrawObject() vypadat nějak takhle :
void DrawObject(Matrix *m_object, Matrix *m_camera,
Object *p_object, unsigned __int32 *video)
{
Face *cur_face, face;
float tx, ty, tz;
float x, y, z;
Matrix m, im;
Polygon poly;
Vertex v[3];
int i, j;
face.vertex[0] = &v[0];
face.vertex[1] = &v[1];
face.vertex[2] = &v[2];
Matrix_Inverse(m_camera, &m);
Matrix_Multiply(&m, m_object, &m);
Matrix_Inverse(&m, &im);
for (i = 0; i < p_object->n_face_num; i ++){
if ((p_object->face[i].normal.c * im[3][2] +
p_object->face[i].normal.a * im[3][0] +
p_object->face[i].normal.b * im[3][1] +
p_object->face[i].normal.d) > 0.1)
continue ;
cur_face = &p_object->face[i];
for (j = 0; j < 3; j ++){
x = cur_face->vertex[j]->x;
y = cur_face->vertex[j]->y;
z = cur_face->vertex[j]->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];
face.vertex[j]->x = tx;
face.vertex[j]->y = ty;
face.vertex[j]->z = tz;
}
poly = Clip_Face(&face, &pyramid);
if (poly.n_vertex_num) { for (j = 0; j < poly.n_vertex_num; j ++) {
if (poly.vertex[j].z < 0.1) {
poly.n_vertex_num = 0;
break ;
}
poly.vertex[j].x = (n_Width / 2) + (poly.vertex[j].x *
z_delta) / poly.vertex[j].z;
poly.vertex[j].y = (n_Height / 2) + (poly.vertex[j].y *
z_delta) / poly.vertex[j].z;
}
DrawPolygon(&poly, video);
}
}
}
|
Je to krásné, jasné a čisté. Ale jde to ještě trochu vylepšit. Jestli jste si to pořádně
prohlídli, asi jste zjistili, že face se napřed transformuje a až potom se zjišťuje, jestli
vůbec bude vidět v klipovací pyramidě. Tenhle proces jde i obrátit a jde transformovat
klipovací pyramida. Není to sice tak jednoduché, jako transformovat vektor, ale zase moc
složitá to taky není. (takže se to vyplatí - většinou) Dělá se to asi takhle :
void TransformPlane(Plane *p, Matrix m)
{
p->ta = p->a * m[0][0] + p->b * m[1][1] + p->c * m[1][2];
p->tb = p->a * m[1][0] + p->b * m[1][1] + p->c * m[0][2];
p->tc = p->a * m[2][0] + p->b * m[2][1] + p->c * m[2][2];
p->td = p->d - (p->ta * m[3][0] + p->tb * m[3][1] + p->tc * m[3][2]);
}
|
A je to. Otázkou je, o kolik rychlejší to bude, protože se zase bude transformovat víc
vertexů na rozdělených polygonech. Záleží na tom, jestli svět bude 'rotující brambora',
nebo třeba aréna z Ut / Q3 (cca 10.000 polygonů). No, ještě přepíšu draw object (pro
jistotu) a vrhneme se na rasterizaci polygonů.
void DrawObject(Matrix *m_object, Matrix *m_camera,
Object *p_object, unsigned __int32 *video)
{
Face *cur_face, face;
float tx, ty, tz;
float x, y, z;
Matrix m, im;
Polygon poly;
Vertex v[3];
int i, j;
face.vertex[0] = &v[0];
face.vertex[1] = &v[1];
face.vertex[2] = &v[2];
Matrix_Inverse(m_camera, &m);
Matrix_Multiply(&m, m_object, &m);
Matrix_Inverse(&m, &im);
TransformPlane(&pyramid.p0, camera);
TransformPlane(&pyramid.p1, camera);
TransformPlane(&pyramid.p2, camera);
TransformPlane(&pyramid.p3, camera);
for (i = 0; i < p_object->n_face_num; i ++){
if ((p_object->face[i].normal.c * im[3][2] +
p_object->face[i].normal.a * im[3][0] +
p_object->face[i].normal.b * im[3][1] +
p_object->face[i].normal.d) > 0.1)
continue ;
poly = Clip_Face(&p_object->face[i], &pyramid);
if (!poly.n_vertex_num)
continue ;
for (j = 0; j < poly.n_vertex_num; j ++) {
x = cur_face->vertex[j]->x;
y = cur_face->vertex[j]->y;
z = cur_face->vertex[j]->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];
if ((poly.vertex[j].z = tz) < 0.1) {
poly.n_vertex_num = 0;
break ;
}
poly.vertex[j].x = (n_Width / 2) + (tx * z_delta) / tz;
poly.vertex[j].y = (n_Height / 2) + (ty * z_delta) / tz;
}
if (poly.n_vertex_num)
DrawPolygon(&poly, video);
}
}
|
No a jak jsem sliboval na začátku, dáme si ještě rasterizaci polygonů, abysme to lépe
viděli :-) Jestli si pamatujete, jak jsem ukazoval Brenshamův algoritmus pro kreslení
čar, tak s polygony je to podobné. Vygeneruje se datová struktura, zvaná edge pro každou
hranu polygonu, která není vodorovná (pak ji nahradí její okolní hrany). Hrany jsou
ve dvou listech (pro levou a pravou polovinu polygonu). To se určí tak, že se vezme bod
s nejmenší a největší y (2d) souřadnicí a pravá strana je od vrcholu s min. y po vrchol
s max. y a levá od vrcholu s max. y po vrchol s min. y (=> záleží na pořadí vrcholů - 3d
studio je má po směru hod. ručiček - myslím :-| ). Potom se vezme pravá a levá hrana
a jde se po obrazových řádkách odshora dolů a pro každý řádek se určí dvě x-ové souřadnice
a od té menší k té větší se udělá jednoduše čára (bez textury - zatím). Napřed se podíváme
na ty edge :
struct Poly_Edge {
int x, y, num, dxdy;
};
static Poly_Edge left_edge_buff[7];
static Poly_Edge right_edge_buff[7];
static Poly_Edge *p_left_edge;
static Poly_Edge *p_right_edge;
static int n_left_edge_num, n_right_edge_num;
|
Někteří si možná říkáte, proč je dxdy integer ? Vždyť to přece nemůže stačit ! Ale on to
není zase až tak moc integer. Je to malá programátorská finta, kterou použijeme ještě
mnohokrát. Říká se tomu "fixed point". Tohle je konkrétně 16:16 fixed point. To znamená,
že desetinná část čísla zabere 16 bitů a celá taky. Dělá se to tak, že se float - hodnota
vynásobí 0x10000 (2 na 16 = 65536) a zaokrouhlí. Pak zabere 32 bitů a mezi sebou se pak
můžou normálně násobit a sčítat nebo cokoli jiného. Když chcete zpátky hodnotu čísla,
vydělíte ho zase 0x10000 a dostanete float. Pokud vám stačí integer, použijete rychlejší
posun o šestnáct míst doprava. Kdyby někdo nevěděl jak to v c-čku zapsat, tak je to
nějak takhle : int = fix >> 16. Jasno ? Fixed point ušetří hodně času procesoru,
když se s ním umí zacházet. Pro fixed point (tj. integer) se totiž používají jen
integerovské operace. A ty jsou rychlejší, než float - operace. No, zase jsme se trochu
zdrželi, je na čase ukázat si, jak se vygeneruje taková struktura edge.
int Add_Edge(Vertex *v_cur, Vertex *v_next, Poly_Edge *edge)
{
float y1, y2;
int num;
y1 = (float )ceil(v_cur->y);
y2 = (float )ceil(v_next->y);
num = (int )(y2 - y1);
if (!num)
return 0;
edge->dxdy = (int )((v_next->x - v_cur->x) / (v_next->y - v_cur->y) * 65536);
edge->x = (int )(v_cur->x * 65536 + (y1 - v_cur->y) * edge->dxdy);
edge->y = (int )y1;
edge->num = num;
return 1;
}
int Create_Edges(Polygon *p_poly)
{
int n_bottom, n_top, n_cur;
float f_min_y;
float f_max_y;
n_left_edge_num = 0;
n_right_edge_num = 0;
f_min_y = 0x10000;
f_max_y = -0x10000;
n_cur = 0;
while (n_cur < p_poly->n_vertex_num) {
if (p_poly->vertex[n_cur].y < f_min_y) {
f_min_y = p_poly->vertex[n_cur].y;
n_top = n_cur;
}
if (p_poly->vertex[n_cur].y > f_max_y) {
f_max_y = p_poly->vertex[n_cur].y;
n_bottom = n_cur;
}
n_cur ++;
}
if ((int )ceil(f_min_y) >= (int )ceil(f_max_y))
return 0;
n_cur = n_top;
while (n_cur != n_bottom) {
if (Add_Edge(&p_poly->vertex[n_cur], &p_poly->vertex[(n_cur + 1) %
p_poly->n_vertex_num], &right_edge_buff[n_right_edge_num]))
n_right_edge_num ++;
n_cur = (n_cur + 1) % p_poly->n_vertex_num;
}
n_cur = n_bottom;
while (n_cur != n_top) {
if (Add_Edge(&p_poly->vertex[(n_cur + 1) % p_poly->n_vertex_num],
&p_poly->vertex[n_cur], &left_edge_buff[n_left_edge_num]))
n_left_edge_num ++;
n_cur = (n_cur + 1) % p_poly->n_vertex_num;
}
p_left_edge = left_edge_buff + n_left_edge_num - 1;
p_right_edge = right_edge_buff;
return 1;
}
|
No, máme vygenerované edge a už stačí jen vykreslovat, ale nejen ti pozornější si jistě
všimli, že polygony jsou kreslené jen jednou barvou. To bysme ale pořádně neviděli ani,
jak objekty vypadají a tak ještě přidáme něco jednoduchého. Je to stínování. Metoda,
kterou si budeme popisovat se jmenuje "flat shading" a je to určení barvy podle normálového
vektoru facu. Když se natransformuje, tak jeho hodnota z (musí být normalizovaný udává
vlastně sklon vůči kameře - světlost) -> stačí jen násobit r, g a b touto hodnotou a pak
dopočítat výslednou barvu v RGB. Takže to vypadá nějak takhle :
void DrawObject(Matrix *m_object, Matrix *m_camera,
Object *p_object, unsigned __int32 *video, unsigned __int32 color)
{
unsigned __int32 n_color;
float tx, ty, tz;
float x, y, z;
Matrix m, im;
Polygon poly;
Vertex v[3];
Face *cur_face, face;
int i, j;
face.vertex[0] = &v[0];
face.vertex[1] = &v[1];
face.vertex[2] = &v[2];
Matrix_Inverse(m_camera, &m);
Matrix_Multiply(&m, m_object, &m);
Matrix_Inverse(&m, &im);
for (i = 0; i < p_object->n_face_num; i ++){
if ((p_object->face[i].normal.c * im[3][2] +
p_object->face[i].normal.a * im[3][0] +
p_object->face[i].normal.b * im[3][1] +
p_object->face[i].normal.d) > 0.1)
continue ;
cur_face = &p_object->face[i];
for (j = 0; j < 3; j ++){
x = cur_face->vertex[j]->x;
y = cur_face->vertex[j]->y;
z = cur_face->vertex[j]->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];
face.vertex[j]->x = tx;
face.vertex[j]->y = ty;
face.vertex[j]->z = tz;
}
poly = Clip_Face(&face, &pyramid);
x = p_object->face[i].normal.a;
y = p_object->face[i].normal.b;
z = p_object->face[i].normal.c;
tx = x * m[0][0] + y * m[1][0] + z * m[2][0];
ty = x * m[0][1] + y * m[1][1] + z * m[2][1];
tz = x * m[0][2] + y * m[1][2] + z * m[2][2];
x = (float )sqrt(tx * tx + ty * ty + tz * tz);
j = abs((int )(tz / x * 255));
if (j > 255)
j = 255;
n_color = ((((color & 0xff0000) * j) & 0xff000000) +
(((color & 0x00ff00) * j) & 0x00ff0000) +
(((color & 0x0000ff) * j) & 0x0000ff00)) >> 8;
if (poly.n_vertex_num) { for (j = 0; j < poly.n_vertex_num; j ++) {
if (poly.vertex[j].z < 0.1) {
poly.n_vertex_num = 0;
break ;
}
poly.vertex[j].x = (n_Width / 2) + (poly.vertex[j].x *
z_delta) / poly.vertex[j].z;
poly.vertex[j].y = (n_Height / 2) + (poly.vertex[j].y *
z_delta) / poly.vertex[j].z;
}
DrawPolygon(&poly, video, n_color);
}
}
}
|
Doufám, že to dokážete takhle rychle vstřebávat. Zatím se nepoužívá transformace
klipovací pyramidy, ať máte doma taky co zkoušet. Teď už nám schází jen funkce
DrawPolygon(poly, video, color). Tak hrr na ní :
int DrawPolygon(Polygon *p, unsigned __int32 *video, unsigned __int32 color)
{
int start_y, left_x, right_x;
int l, c;
if (!Create_Edges(p))
return 0;
start_y = p_right_edge->y;
left_x = p_left_edge->x;
right_x = p_right_edge->x;
video += n_Width * start_y;
while (1) {
c = left_x >> 16;
l = right_x >> 16;
for (; l < c; l ++)
video[l] = color;
video += n_Width;
z_buff += n_Width;
z += dzdy;
if (-- p_left_edge->num == 0) {
if (p_left_edge -- == left_edge_buff)
return 1;
left_x = p_left_edge->x;
} else
left_x += p_left_edge->dxdy;
if (-- p_right_edge->num == 0) {
p_right_edge ++;
right_x = p_right_edge->x;
} else
right_x += p_right_edge->dxdy;
}
return 1;
}
|
No.. tolik zdrojáku jsem ještě za jeden den nenapsal (myslím). Tentokrát vám dám jenom
krátkej sampl bez zdrojáků. No a jestli to někdo bude číst (třeba jen jeden nebo líp jedna)
tak mi napíše hezkej email (a nebo víc :-) ) a já budu v příštím díle pokračovat s tím
Z-Bufferem. (Teď se spráně vykreslují jen konvexní útvary - kostka, koule, pyramida ...)
Můžete zkusit seřadit facy podle vzdálenosti středu od kamery a pak to bude trochu fungovat,
když se objekty nebudou protínat. Jestli jste hráli Tomb - Raidera I nebo ][, tak víte,
o čem mluvím. Odborně se tomu říká "malířův algoritmus", ale ten si zkuste sami.
Tož se mejte !
-tHE SWINe-
Tomb raider 2 - Lara má menší polygony než plošina a levá noha se seřadila špatně
Flat - shading
Malířův algoritmus
Valid HTML 4.01
|