|
3D - Engine 05 - z-buffer a texture-mapping
Hallejujah ! Jestliže věříte v boha, děkujte mu za další pokračování seriálu o 3D Enginech,
který už mnozí z vás začali pokládat za dead project. Ale jsem tu zas s novým dílem (a taky
s kupou novejch průserů, ale o ty se s váma nemíním dělit :-) ). Plánuju, že dneska si povíme
o texturování a z-bufferování. Předem bych chtěl upozornit, že inspirací byl krásný dokument,
zvaný "Fatmap" (Fast affine texture mapping) od Matse Byggmastara a.k.a. MRI / Doomsday,
originál si můžete najít na internetu, je mnohem optimalizovanější, ale nahlíží na problém
z trochu jiné strany (podobné jsou jen názvy některých proměnných a základní nápad mapování).
Asi první, kdo dokázal udělat texturemapping "realtime" byl >Doomtvůrce< Adrian Carmack,
který ho dokázal optimalizovat tak, abychom si mohli večer krásně zapařit Dooma i na i486
hezky plynule :-) ... Možná se někteří ptáte, co má společného texturemapping a Z-Buffer.
Povím vám o co jde : Z-Buffer (někdo to překládá jako paměť hloubky) je pomůcka, jak zjistit,
jestli bod bude na výsledném obrázku vidět. Je to vlastně pole o stejné velikosti jako
obrazovka, které se napřed naplní nulovými hodnotami (zpravidla jde o fixed point) a potom když
se kreslí nějáký bod polygonu, program se zeptá, jestli už tam není nějaký bod, který leží
blíž než ten, co se zrovna kreslí. Pokud ne, hodnota v Z-Bufferu se přepíše novou hodnotou
a do videopaměti se vykreslí nový bod, který překryje ten předchozí. Pokud už tam je, nic
se nedělá a jde se na další pixel.
Mezi výhody Z-Bufferu patří třeba perfektní prolínání objektů :
Mezi nevýhody patří třeba to, že je hodně pomalý : Na vykreslení každého bodu musíte porovnat
číslo a body se často mnohokrát překreslují. Z-Buffer navíc vyžaduje poměrně přesné ukládání
čísel a tak zabere hodně paměti. Ale pořád jsem vám ještě neřekl tu spojitost s texturováním.
Tou je souřadnice Z, která se musí "rozmáznout" po celém povrchu polygonu. Dělá se to tak,
že pro první tři souřadnice se vytvoří rovina (rovina je definovaná třemi body), přičemž
souřadnice jsou 2D - x, 2D - y a 3D - z (to znamená, že x a y jsou perspektivně korigované,
ale z je to, co "vylezlo" z transformace maticí). Rovnice roviny zní :
Potom si můžeme vyjádřit z-ovou souřadnici jako rovnici :
No a z toho vidíme, že tu jsou nějaké konstanty, které si můžeme předpočítat a použít
při mapování :
Přičemž dzdx je "delta z / delta x", dzdy je "delta z / delta y" a begz je souřadnice z v bodě
x = 0, y = 0 (levý horní roh obrazovky). Už vám to začíná docházet ? Prostě spočítáme
souřadnici pro každý řádek a pak už k ní jen přičítáme dzdx po každém pixelu. Už nám schází jen
to D-čko. To si odvodíme jinak a spočítáme přímo :
V c-čku by se to všechno spočítalo asi takhle :
struct Vertex {
float x, y, z;
float u, v;
float illum;
Vector normal;
};
struct Polygon {
int n_vertex_num;
Vertex vertex[7];
};
float didx, didy, begi;
int Compute_Interpolation_Consts(POLYGON *p)
{
float x[2], y[2], l[2];
float a, b, c;
x[0] = p->vtx[1].x - p->vtx[0].x;
x[1] = p->vtx[2].x - p->vtx[0].x;
y[0] = p->vtx[1].y - p->vtx[0].y;
y[1] = p->vtx[2].y - p->vtx[0].y;
l[0] = (float )p->vtx[1].illum - (float )p->vtx[0].illum;
l[1] = (float )p->vtx[2].illum - (float )p->vtx[0].illum;
a = y[0] * l[1] - y[1] * l[0];
b = l[0] * x[1] - l[1] * x[0];
c = x[0] * y[1] - x[1] * y[0];
if (c == 0)
return 0;
didx = - a / c;
didy = - b / c;
begi = (float )p->vtx[0].illum - p->vtx[0].x * didx - p->vtx[0].y * didy;
return 1;
}
int DrawPolygon(Polygon *p, unsigned __int32 *video, unsigned __int32 color)
{
int start_y, left_x, right_x;
int didx_fp, k, r_k;
float illum;
int l, c;
if (!Create_Edges(p))
return 0;
if (!Compute_Interpolation_Consts(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;
illum = begi + didy * start_y;
didx_fp = (int )(didx * 65536);
while (1) {
l = left_x >> 16;
c = right_x >> 16;
k = (int )(illum * 65536) + c * didx_fp;
for (; c < l; c ++) {
r_k = k >> 16;
if (r_k > 255)
r_k = 255;
if (r_k < 0)
r_k = 0;
video[c] = ((((color & 0xff0000) * r_k) & 0xff000000) +
(((color & 0x00ff00) * r_k) & 0x00ff0000) +
(((color & 0x0000ff) * r_k) & 0x0000ff00)) >> 8;
k += didx_fp;
}
video += n_Width;
illum += didy;
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;
}
|
Nakonec to ale nebude tak jednoduché, jak se zdá, protože takhle rozkládáme souřadnici lineárně
(a proto taky rychle), ale v normálním životě "textury nejsou lineární" :
Je vidět jak čtverce blíž kamery jsou větší, než ty dál od kamery..
No, ale abych vám nezkazil radost, je tady zdroják pro jednoduchý engine se Smooth shading
(to se určí pseudonormálové vektory jednotlivých vertexů zprůměrováním normálových vektorů
faců, které mají ten vrchol společný - prohlédněte si funkci ComputeVertexNormals() v modulu
load3ds.cpp a tak se určí světlost jako při flat shading, ale pro jednotlivé vrcholy. Potom
už se světlost jen interpoluje po povrchu a naštěstí není vidět, že se interpoluje lineárně).
Běží to pod DirectX 7.01, můžete si stáhnout nový modul, jen v Project > Settings
v záložce Link musíte přidat kromě "ddraw.lib" ještě "dxguid.lib" do seznamu knihoven.
(Budete si asi muset stáhnout knihovny a hlavičky pro práci s direct x ze sekce downloads
a nakopírovat je do lib a include složek vašeho kompilátoru ...)
Smooth engine - Font engine + Antialiassing
Doufám, že se vám to bude líbit :-), dal jsem si s tím docela záležet. Používá to zatím ještě
pořád malířův algoritmus, má to jednoduchý modul pro kreslení fontů a používá to skoro-Quincunx
pro antialiasování výsledného obrazu. Ale teď půjdeme dál..
Perspektivně korektní mapování
Už jsem řekl, že mapování nebude lineární.. No jo, ale co s tím ? Chceme mít výhody lineárního
mapování, ale potřebujeme mapování hyperbolické... Někdo chytrý vymyslel trik, převádějící
nelineární závislost na lineární, a pak ho použil ve své hře (Quake ?). Klidně si stáhněte
z www.IDSoftware.com zdrojáky Quaka 1 a tam si ten trik můžete najít. Funguje to tak, že
hyperbolickou funkci (y = 1 / x) převedeme na lineární (x) a když potřebujeme její hodnotu (y),
musíme zase zpětně převádět zpátky. Zpětný převod je však dost náročný, takže opět ten někdo
vymyslel malou optimalizaci : Funkce se bude zpětně korigovat každý n-tý pixel a mezi nimi
se závislost bude brát jako lineární :
Opticky tuto chybu téměř nemáte šanci postihnout, ale optimalizace je výrazná (v praxi by to
bylo několik - podle počtu interpolovaných souřadnic - dělení na každý pixel). Tentokrát
se nebude mapovat Z, ale 1/ Z a každá souřadnice (mapovaná, ne x a y) bude na 1/ Z závislá.
Potom to bude vypadat asi takhle :
int n_Width, n_Height;
__int32 *z_buffer;
float dzdx, dzdy, begz;
int Compute_Interpolation_Consts(Polygon *p_poly)
{
float x[2], y[2], z[2];
float a, b, c;
x[0] = p_poly->vertex[1].x - p_poly->vertex[0].x;
x[1] = p_poly->vertex[2].x - p_poly->vertex[0].x;
y[0] = p_poly->vertex[1].y - p_poly->vertex[0].y;
y[1] = p_poly->vertex[2].y - p_poly->vertex[0].y;
z[0] = (1 / p_poly->vertex[1].z) - (1 / p_poly->vertex[0].z);
z[1] = (1 / p_poly->vertex[2].z) - (1 / p_poly->vertex[0].z);
a = y[0] * z[1] - y[1] * z[0];
b = z[0] * x[1] - z[1] * x[0];
c = x[0] * y[1] - x[1] * y[0];
if (c == 0)
return 0;
dzdx = - a / c;
dzdy = - b / c;
begz = (1 / p_poly->vertex[0].z) -
p_poly->vertex[0].x * dzdx -
p_poly->vertex[0].y * dzdy;
return 1;
}
void Interpolate_Segment(int z1, int dz, unsigned __int32 *video,
unsigned __int32 *v2, __int32 *z_buff, unsigned __int32 color)
{
do {
if ((unsigned )(*z_buff) > (unsigned )z1){
*video = color;
*z_buff = z1;
}
video ++;
z_buff ++;
z1 += dz;
} while (video < v2);
}
void Draw_Segment(int c, int l, float z, __int32 *z_buff,
unsigned __int32 *video, unsigned __int32 color)
{
int dz, z1, z2;
z1 = (int )(0x10000 / z);
while (c >= 16) {
z += dzdx * 16;
z2 = (int )(0x10000 / z);
dz = (z2 - z1) / 16;
Interpolate_Segment(z1, dz, video, video + 16, z_buff, color);
c -= 16;
z_buff += 16;
video += 16;
z1 = z2;
}
if (c > 0) { z += dzdx * c;
z2 = (int )(0x10000 / z);
dz = (z2 - z1) / 16;
Interpolate_Segment(z1, dz, video, video + c, z_buff, color);
}
}
int DrawPolygon(Polygon *p, unsigned __int32 *video, unsigned __int32 color)
{
int start_y, left_x, right_x;
__int32 *z_buff;
int l, c;
float z;
if (!Create_Edges(p))
return 0;
if (!Compute_Interpolation_Consts(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;
z_buff = &z_buffer[n_Width * start_y];
z = begz + dzdy * start_y;
while (1) {
c = left_x >> 16;
l = right_x >> 16;
c -= l ++;
if (c > 0)
Draw_Segment(c, l, z + dzdx * l, z_buff + 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;
}
void Clean_ZBuffer()
{
int i, m;
m = Width * Height;
for (i = 0; i > m; i ++)
z_buffer[i] = 0xffffffff;
}
|
Ještě se podívejte na obrázek :
Z_Buffer + Flat shading
No.. A máme tu engine se z-bufferem. Funguje to krásně přesně a pomalu. Proto hned v příštím
díle bude popsaná trochu jiná technika na určování viditelnosti, jmenující se BSP Strom.
Ale o tom až příště. Teď jsem ještě slíbil ty textury. Napřed budeme potřebovat ty textury
z něčeho nahrát. Asi nejjednodušší je pro nás 24 Bpp Bitmapa. Možná jste si všimli, že rutina
pro nahrávání bitmap je v text-enginu toho enginu se Smooth shading. Pro jistotu ji sem dám
ještě jednou :
#include <windows.h>
#include <stdio.h>
struct BMP {
unsigned __int32 *bytes; int width, height; };
BMP *Load_TrueColorBMP(char *path)
{
BITMAPFILEHEADER bmfh;
BITMAPINFOHEADER bmih;
int Width, Height;
char *lpbBuf;
int BuffLen;
FILE *BMPfp;
BMP *tmp;
int i, j;
if ((BMPfp = fopen(path, "rb")) == NULL)
return NULL;
fread(&bmfh, sizeof (bmfh), 1, BMPfp);
fread(&bmih, sizeof (bmih), 1, BMPfp);
if (bmfh.bfType != ('B'|('M' << 8)))
return NULL;
if (bmih.biBitCount == 24 && bmih.biCompression == BI_RGB) {
if ((tmp = (BMP*)malloc(sizeof (BMP))) == NULL)
return NULL;
Width = bmih.biWidth;
Height = bmih.biHeight;
tmp->n_Width = Width;
tmp->n_Height = Height;
if ((tmp->bytes = (unsigned __int32 *)malloc(Width * Height * 4)) == NULL)
return NULL;
BuffLen = ((Width * 3 + 3) >> 2) << 2;
if ((lpbBuf = (char *)malloc(BuffLen)) == NULL)
return NULL;
for (i = Height - 1; i >= 0; i --) {
if ((j = fread(lpbBuf, 1, BuffLen, BMPfp)) != BuffLen)
return NULL;
for (j = 0; j < Width; j ++) {
tmp->bytes[j + Width * i] = RGB(lpbBuf[j * 3],
lpbBuf[j * 3 + 1], lpbBuf[j * 3 + 2]);
}
}
} else
return NULL;
free(lpbBuf);
return tmp;
}
|
No, doufám že to nemusím popisovat.. Napřed se načtou hlavičky bitmapy, určí se zda je bitmapa
24 bpp a RGB - nekomprimovaný truecolorový rastr. Potom se naalokuje pole pro pixely
ve struktuře BMP a buffer pro čtení jednotlivých řádků obrazu (musí být zarovnaný na 32 bit)
a pak už se jen načítá z disku..
Tak.. Ještě jsem pořád nevysvětlil, jak vygenerovat souřadnice pro textury.. Říkal jsem
jen že jsou závislé na z, takže se na to vrhneme, už je to stejně moc dlouhý povídání :
int Compute_Interpolation_Consts(Polygon *p_poly)
{
float x[2], y[2], z[2], u[2], v[2];
float a, b, c;
x[0] = p_poly->vertex[1].x - p_poly->vertex[0].x;
x[1] = p_poly->vertex[2].x - p_poly->vertex[0].x;
y[0] = p_poly->vertex[1].y - p_poly->vertex[0].y;
y[1] = p_poly->vertex[2].y - p_poly->vertex[0].y;
z[0] = (1 / p_poly->vertex[1].z) - (1 / p_poly->vertex[0].z);
z[1] = (1 / p_poly->vertex[2].z) - (1 / p_poly->vertex[0].z);
u[0] = 1 / p_poly->vertex[1].z * p_poly->vertex[1].u -
1 / p_poly->vertex[0].z * p_poly->vertex[0].u;
u[1] = 1 / p_poly->vertex[2].z * p_poly->vertex[2].u -
1 / p_poly->vertex[0].z * p_poly->vertex[0].u;
v[0] = 1 / p_poly->vertex[1].z * p_poly->vertex[1].v -
1 / p_poly->vertex[0].z * p_poly->vertex[0].v;
v[1] = 1 / p_poly->vertex[2].z * p_poly->vertex[2].v -
1 / p_poly->vertex[0].z * p_poly->vertex[0].v;
a = y[0] * z[1] - y[1] * z[0];
b = z[0] * x[1] - z[1] * x[0];
c = x[0] * y[1] - x[1] * y[0];
if (c == 0)
return 0;
dzdx = - a / c;
dzdy = - b / c;
begz = (1 / p_poly->vertex[0].z) -
p_poly->vertex[0].x * dzdx -
p_poly->vertex[0].y * dzdy;
a = y[0] * u[1] - y[1] * u[0];
b = u[0] * x[1] - u[1] * x[0];
dudx = - a / c;
dudy = - b / c;
begu = (1 / p_poly->vertex[0].z * p_poly->vertex[0].u) -
p_poly->vertex[0].x * dudx - p_poly->vertex[0].y * dudy;
a = y[0] * v[1] - y[1] * v[0];
b = v[0] * x[1] - v[1] * x[0];
dvdx = - a / c;
dvdy = - b / c;
begv = (1 / p_poly->vertex[0].z * p_poly->vertex[0].v) -
p_poly->vertex[0].x * dvdx - p_poly->vertex[0].y * dvdy;
return 1;
}
|
Pak je použijete úplně stejně jako z-souřadnici v tom minulým zdrojáku :
int DrawPolygon(Polygon *p, unsigned __int32 *video)
{
int start_y, left_x, right_x;
__int32 *z_buff;
float z, u, v;
int l, c;
if (!Create_Edges(p))
return 0;
if (!Compute_Interpolation_Consts(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;
z_buff = &z_buffer[n_Width * start_y];
z = begz + dzdy * start_y;
u = begu + dudy * start_y;
v = begv + dvdy * start_y;
while (1) {
c = left_x > 16;
l = right_x > 16;
c -= l ++;
if (c &ht; 0) {
Draw_Segment(c, l, u + dudx * l, v + dvdx * l,
z + dzdx * l, z_buff + l, video + l);
}
video += n_Width;
z_buff += n_Width;
z += dzdy;
u += dudy;
v += dvdy;
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;
}
void Draw_Segment(int c, int l, float u, float v, float z,
__int32 *z_buff, unsigned __int32 *video)
{
float dz, z1, z2;
int du, u1, u2;
int dv, v1, v2;
z1 = (0x10000 / z);
u1 = (int )(u * z1);
v1 = (int )(v * z1);
while (c >= 16) {
z += dzdx * 16;
u += dudx * 16;
v += dvdx * 16;
z2 = (0x10000 / z);
u2 = (int )(u * z2);
v2 = (int )(v * z2);
dz = (z2 - z1) / 16;
du = (u2 - u1) / 16;
dv = (v2 - v1) / 16;
Interpolate_Segment(u1, v1, z1, du, dv, dz, video, video + 16, z_buff);
c -= 16;
z_buff += 16;
video += 16;
z1 = z2;
u1 = u2;
v1 = v2;
}
if (c > 0) { z += dzdx * c;
u += dudx * c;
v += dvdx * c; z2 = (0x10000 / z);
u2 = (int )(u * z2);
v2 = (int )(v * z2);
dz = (z2 - z1) / c; du = (u2 - u1) / c;
dv = (v2 - v1) / c;
Interpolate_Segment(u1, v1, z1, du, dv, dz, video, video + c, z_buff);
}
}
|
Tak, interpolate_segment() dostane souřadnice textury v 16:16 fixed pointu. Mohli byste je
normálně převést na reálné číslo a vynásobit tak, aby daly index pixelu v textuře, ale to by
bylo zbytečně pomalé. Je tu finta s bitovými posuny, ale je tu jedna podmínka : Textura musí
mít rozměry o velikosti mocniny dvou. A teď už se podíváme jak to bude vypadat :
void Interpolate_Segment(int u1, int v1, int z1,
int du, int dv, int dz, unsigned __int32 *video,
unsigned __int32 *v2, __int32 *z_buff)
{
do {
if ((unsigned )(*z_buff) > (unsigned )z1) {
*video = _texture[((u1 >> 16) & 0xff) + ((v1 >> 8) & 0xff00)];
*z_buff = z1;
}
video ++;
z_buff ++;
z1 += dz;
u1 += du;
v1 += dv;
} while (video < v2);
}
|
Tak, je to stará smyčka ze Z-Bufferu, jen místo barvy se nanáší textura.. u1 >> 16 je převod
souřadnice U (x-ová souřadnice v textuře) z 16:16 na reál a & 0xff je její oříznutí na interval
hodnot 0 - 255 (textura bude mít šířku 256). Zápis v1 >> 8 je převod v1 na reál. a zároveň
ještě jeho násobení 256-ti. Log. součin s 0xff00 je opět oříznutí. Dohromady to tedy
dá 0xff + 0xff00 = 0xffff = 65536. To je 256 × 256, tedy i výška textury bude 256. Jak to ale
změnit ? Je potřeba vygenerovat tři čísla : 0xff, 8 a 0xff00. Když to zkrátím bez vysvětlení,
je to takhle :
Width = 256;
Height = 256;
0xff = Width - 1;
0xff00 = Width * (Height - 1);
8 = 16 - log(Height) / log(2);
|
Tak už to nečtite, stáhnite si zdroják a naschle příště :
Z_Buffer + Texturing
Valid HTML 4.01
|