Текстуриране
Поставянето на текстура върху обект не е едно от най-лесните неща на този свят :) , но бъдете сигурни, че ще бъдете щастливи, когато го овладеете, а това не е невъзможно и даже би било лесно, ако четете задълбочено. Ще ви представя и Mipmapping технологията, която е сравнително нова и игрите през последните 5 години използват точно нея.

Когато използвате текстура, нищо не ви пречи да използвате динамична светлина ( ще ви покажа по-късно някои особенности ). Просто както вече знаете трябва да определите светлината и материала на обекта и всичко е готово. Изображенията могат да се заредят най-лесно от *.bmp файл , както ще ви покажа след малко. След като заредите изображението, трябва да бъде построена текстура и след това съхранена в масив, откъдето може да се използва за различни обекти.

Първото нещо което трябва да направите е да разрешите 2D текстурирането glEnable( GL_TEXTURE_2D ) във вашата main( ) функция или друга подобна, която се извиква преди рендериращата функция.

След това е нужно да определим по-какъв начин ще се наслагва текстурата върху обекта. Това става с функцията :

void glTexEnv{if}( GLenum target, GLenum pname, GLint param ) - тя приема три аргумента. Първият трябва да е GL_TEXTURE_ENV, вторият аргумент - GL_TEXTURE_ENV_MODE, a последният може да бъде една от константите :

GL_MODULATE - цвета на пикселите от текстурата се умножава по цвета на обекта.
GL_DECAL - цвета на пикселите от текстурата заменя цвета на обекта.
GL_BLEND - цвета на текстурата се смесва с предварително определен цвят-константа и след това с цвета на обекта. За да определите константния цвят трябва да извикате функцията :

void glTexEnvfv( GLenum target, GLenum pname, const GLint *params ) - тя приема 3 аргумента. Първия трябва да е отново GL_TEXTURE_ENV, но за втори аргумент вече трябва да подадете GL_TEXTURE_ENV_COLOR, а третия аргумент е съответно RGBА масива с цвета.

Когато използвате динамична светлина, както знаете вече, тя се отразява на цвета на обекта. Например ако цвета на обекта е бял, при използването на динамичната светлина по-неосветените части стават по-тъмни и дори сиви. Ако искате динамичната светлина да се отразява на повърхността на текстурата трябва да изберете опцията GL_MODULATE. Така всеки път когато цвета на пиксела на текстурата се умножи по този на обекта ( не забравяйте че при включена светлина цвета на обекта прелива в зависимост от количеството на осветяване ) се получава илюзията, че текстурата рефлектира не светлината, а не цвета на обекта. Но в крайна сметка ефекта се получава - имаме текстура, която се променя в зависимост от идващата светлина.

След това трябва да заредим изображението т.е. да конвертираме в масив от RGB стойности. Това е нещo трудно, както за изпълнение така и за разбиране, но за щастие в библиотеката GLAUX съществува функция която конвертира автоматично *.bmp файл. Ето и нейния прототип :

AUX_RGBImageRec* auxDIBImageLoad("име.bmp") - както виждате тя приема името на bmp файла и връща указател към структурна променлива от тип AUX_RGBImageRec. Структурата AUX_RGBImageRec е дефинирана в библиотеката GLAUX и съдържа няколко члена. Това са sizeX , sizeY от тип int, в които се запазват съответно ширината и дължината на зареденото изображение и *data от тип unsigned char, където се запазват RGB стойностите на изображението. Ето как примерно можете да заредите изображението wood.bmp :

AUX_RGBImageRec *image;
image = auxDIBImageLoad("wood.bmp");


Следващата ни стъпка е да генерираме "име" за всяка една от текстурите ни. Това става с функцията :

void glGenTextures( GLsizei n, GLuint *textures ) - тя приема два аргумента. Първия определя броя на "имената" на текстурите, които искаме да бъдат генерирани, а втория аргумент трябва да бъде адреса на масив от тип unsigned int, в който смятаме те да се съдържат. За да не стане объркване ще поясня, че трябва просто да дадете адреса на първата клетка от нашия масив texture[3] т.е. &texture[0]. Сега е момента да изпаднете в паника : "Защо ни са тези имена на текстури ?!?" Те не са задължителни, но ако нямате такива, ще можете да ползвате оптимално само по една текстура , а ще срещнете и други проблеми, така че генерирайте всеки път имена за вашите текстурите. Ето как можете да създадете имена за три текстури: glGenTextures( 3, &textures[0] ) , като предварително сте дефинирали масива GLuint textures[3].

Сега идва ред да построите вашата текстура/и. Но преди това я си помислете, как OpenGL ще разбере коя от тях да построи / използва в даден момент. Затова трябва да "свържем" всяка текстура със генерираните вече имена от масива, който сме посочили на функцията glGenTextures( ). Това става с функцията :

void glBindTexture( GLenum target, GLuint texture ) - тя приема 2 аргумена. Първият трябва да е константата GL_TEXTURE_2D (понеже нашата текстура има дължина и ширина ), определящ че съответното име се отнася за 2D текстура, а втория - дадена клетка от масива с генерираните вече имена.
Всичко това с имената може да ви изглежда ненормално , но всъщност е изключително полезно. Доста по-добре е да построите първо нужните текстури и после да избирате коя да бъде използвана в даден момент, отколкото да построявате нужната текстура всеки път, като губите предишната, която пък може би ще ви трябва след това отново.

Ето ви нагледен пример: glBindTexture( GL_TEXTURE_2D, texture[0] );

След като вече сме свързали първата текстура с първия инд. номер от масива идва ред тя да бъде построена. За построяването на дадена текстура са възможни две функции. Първата от тях е стандартната функция в OpenGL - glTexImage2D( ) за построяване на 2D текстури, а втория вариант е използването на технологията Mipmapping с функцията gluBuild2DMipmaps( ) от библиотеката GLU, в чиито достойнства ще се убедите след малко. Последното важно нещо което трябва да знаете е, че ако използвате glTexImage2D( ) за построяването на дадена текстура, размерите на вашето изображение трябва да са степен на 2. Примерно: 64х128 или 256х256 и т.н.
Ето и прототипа на функцията glTexImage2D( ) :

void glTexImage2D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border,
                                 GLenum format, GLenum type, const GLvoid *pixels );


Има функции които приемат повече параметри :)) Както виждате настоящата функция приема 9 аргумента. Първия аргумент трябва да е константата GL_TEXTURE_2D, втория аргумент определя нивото на детайлите на текстурата и по принцип е 0, което значи че текстурата си остава каквато е т.е. базово ниво. Третия аргумент определя броя на цветовите компоненти, които се използват за всеки пиксел. В Color-Index режим те са 1, в RGB - 3, a в RGBA съответно - 4. В нашия случай цветовите компоненти ще са 3. Четвъртия и петия аргумент определят ширината и дължината на текстурата, които се съдържат съответно в image->sizeX и image->sizeY, където *image е указател към структурна променлива от тип AUX_RGBImageRec. Шестият аргумент определя ширината на рамката на всяка текстура върху даден обект т.е. пикселите които се изпускат при нанасянето на следваща текстура. За ваше добро е подадете стойност 0 за рамката, защото в противен случай ще срещнете проблеми. Седмият аргумент указва типа на цветовете, които се използват и по принцип е една от константите GL_RGB, GL_RGBA или GL_COLOR_INDEX. В случая - GL_RGB. Осмият аргумент указва типа на данните в масива с RGB стойностите. При използването на библиотеката GLAUX за типа на данните е нужно да подадете аргумента GL_UNSIGNED_BYTE. Последния девети аргумент представлява масива със RGB стойностите за всеки пиксел от нашето изображение т.е. подавате като аргумент: image->data. Ето как изглежда всичко:

glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, image->sizeX, image->sizeY, 0, GL_RGB, GL_UNSIGNED_BYTE,
                         image
->data );

А сега ,както ви обещах, ще ви покажа втория начин за постряване на текстура с функцията gluBuild2DMipmaps( ). Mipmap текстурите са много по-добри от стандартните създадени с glTexImage2D. Тяхното предимство се състои в това, че за разлика от обикновените текстури които биват "скалирани" за да се нагодят за съответната повърхност, при Mipmapping - а се построяват автоматично поредица от текстури с различна големина и качесто, като OpenGL избира най - подходящата текстура за дадения момент. Например ако построите обикновена текстура от високо-качествено изображение, при малко разстояние от камерата до текстурирания обект, всичко ще е нормално, но ако се отдалечите текстурата ще бъде "скалирана" към по-малки размери, ще започне да трепти, което се дължи именно на "скалирането", OpenGL ще продължи да я рендерира със всички детайли, независимо от това че вие вече не ги забелязвате, поради малките им размери, системата ви се натоварва.... нo ако използвате Mipmapping това няма да се случи, защото ще бъде построена нова текстура отговаряща на новите по-малки размери на обекта, няма да има трептене, както и малките детайли, които вече и без това не се забелязват, няма да се съдържат в нея. Новата текстура ще бъде с по-ниско качество, но ще ви изглежда отново красива, понеже сте по - надалеч от нея. Ако пък се приближите близко към обекта ще бъде показана oтново текстурата с най-високо качество. А при Mipmapping текстурите можете да ползвате изображения от всякакъв размер. Ето прототипа на функцията gluBuild2DMipmaps( ) :

int gluBuild2DMipmaps( GLenum target, GLint components, GLint width, GLint height, GLenum format,
                                       GLenum type, const void *data );


Tя приема само 7 аргумента и връща 0 ако няма проблеми при построяването на Mipmap текстурата ( не е задължжително да проверявате какво връща самата функцията ). Първия аргумент трябва да е константата GL_TEXTURE_2D. Втория указва броя на цветовите компоненти и вече знаете какво означава това. В нашия случай те са 3. Четвъртият и петият аргумент определят ширината и дължината на текстурата. Петият - константата показваща типа на цветовете, които се използват - в случая това е GL_RGB, последните два аргумента са типа на данните в масива и самия масив. Не обръщам голямо внимание на отделните компоненти, защото те са аналогични с тези от функцията glTexImage2D( ). Ето отново функцията заедно с подадените аргументи:

gluBuild2DMipmaps( GL_TEXTURE_2D, 3, image->sizeX, image->sizeY, GL_RGB, GL_UNSIGNED_BYTE,
                                  image
->data );

След като сме построили нашата текстура, независимо с коя от двете функции, трябва да изберем и начина на филтрация на текстурата, от което зависи и качеството на текстурата при визуализация. Това става с функцията:

void glTexParameter{if}( GLenum target, GLenum pname, GLint param ) - тя приема 3 аргумента. Първия трябва да е GL_TEXTURE_2D, съответно за 2D текстури. Втория аргумент определя филтъра и може да е един от аргументите :

GL_TEXTURE_MAG_FILTER - филтрация, когато текстурата се увеличава на по-голяма площ от стандартните й размери т.е. качество на текстурата при рендериране на малко разстояние до камерата.
GL_TEXTURE_MIN_FILTER - филтрация, когато текстурата се намалява от стандартните й размери т.е. при големи разстояния от текстурата.
Третият аргумент определя качеството на определената филтрация :
При MAG филтъра можете да изберете един от аргументите: GL_NEAREST или GL_LINEAR, съответно за ниско или висококачествена филтрация. Висококачествената филтрация има и своите недостатъци - тя усложнява допълнително работата на графичния ускорител и от това страда общата производителност.
При MIN филтъра нещата са малко по-сложни. Ако при построяване на текстурата сте използвали функцията glTexImage2D( ) избора ви се свежда отново до аргументите GL_NEAREST и GL_LINEAR, но при използването на gluBuild2DMipmaps( ) вече ви се предоставят цели четири нови аргумента и това са:

GL_NEAREST_MIPMAP_NEAREST
GL_NEAREST_MIPMAP_LINEAR
GL_LINEAR_MIPMAP_NEAREST
GL_LINEAR_MIPMAP_LINEAR

Крайната част от всеки аргумент определя качестовото на самата замяна на Mipmap текстурите ( или по-точно тя бива по-плавна при избирането на филтрация завършваща на LINEAR ). Режимът, който постига най-добро отношения между качество/производителност е GL_LINEAR_MIPMAP_NEAREST, като най-качественият режим разбира се е GL_LINEAR_MIPMAP_LINEAR.
Може би сте виждали в графичните опции на някои игри да има избор между BILINEAR и TRILINEAR филтрация за текстурите. Tова са по-кратките имена на режимите GL_LINEAR_MIPMAP_NEAREST и GL_LINEAR_MIPMAP_LINEAR.
Също така не е забранено да използвате GL_LINEAR и GL_NEAREST, но вече вашата текстура няма да е Mipmap.
Ето нагледно как можете да определите филтрация за една Mipmap текстура:

glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST );


Вече построихме нашите текстури, избрахме режим на филтриране, но все още не сме свършили цялата работа. Представете си, че имате квадратна стена и текстура, но стената е четири пъти по-голяма от текстурата. От това може би се досещате, че трябва да съществува нещо, което да накара текстурата да се "залепи" четири пъти на стената, вместо да разпъваме текстурата до размерите на стената, което ще бъде съпроводено и с развалянето на качеството на текстурата.

Когато определяте координатите на върховете на вашия обект трябва да определите и координати за тесктурата. Това може да ви се стори плашещо :), но не е толкова трудно, колкото си мислите. Координатите на текстурата определят коя точка от текстурата се намира върху съответния връх. Важно е да се отбележи, че вие определяте координатите само веднъж и нататък OpenGL се грижи сам за своята работа, при всякакви завъртания или движения на обекта. Определянето на координатите на текстурата става с функцията: glTexCoord*( ), която има доста разновидности, но най използваната е glTexCoord2f( ). Tя приема 2 стойности съответно за x и y, указващи местоположението на съответната точка от текстурата. Например, ако имате квадрат върху който искате да сложите текстура и започнете да определяте върховете му обратно на часовниковата стрелка, като започнете от долноя ляв ъгъл, то координатите на текстурата ще бъдат съответно : ( 0, 0 ) - първи пиксел от текстурата долу в ляво, ( 1, 0 ) - последен пиксел от текстурата долу в дясно, ( 1, 1 ) - последен пиксел от текстурата горе в дясно и ( 0, 1 ) - последен пиксел от текстурата горе в ляво. Ето и малко код за да стане по-ясно :


glBegin( GL_QUADS );

glTexCoord2f ( 0.0, 0.0 ); glVertex3f ( -1.0 , -1.0f, 0.0 );
// долен ляв ъгъл

glTexCoord2f ( 1.0, 0.0 ); glVertex3f ( 1.0, -1.0f, 0.0 ); // долен десен ъгъл

glTexCoord2f ( 1.0, 1.0 ); glVertex3f ( 1.0, 1.0f, 0.0 ); // горен десен ъгъл

glTexCoord2f ( 0.0, 1.0 ); glVertex3f ( 1.0, 1.0f, 0.0 ); // горен ляв ъгъл

glEnd( );



Ако квадрата беше няколко пъти по - голям, отново щяхте да можете да ползвате същите координати за текстурата, но тя щеше да бъде уголемена, което има своите отрицателни последици за качеството на текстурата. Ако пък имахте правоъгълник, чиято височина е 1/2 от ширината му и отново използвате сегашните текстурни координати, текстурата щеше да бъде сбита откъм височина, което отново може да има своите негативни страни за качеството.
Да вземем за пример първо правоъгълника чиято височина е 1/2 от ширината. Тук текстурните координати трябва да са съответно ( 0, 0 ) - първи пиксел от текстурата долу в ляво, ( 1, 0 ) - последен пиксел от текстурата долу в дясно, ( 1, 0.5 ) - пиксел най-вдясно от средата на текстурата и ( 0, 0.5 ) - пиксел най-вляво от средата на текстурата . Това ще доведе до наслагване само на половината от текстурата и така няма да има "сбиване" до размерите на правоъгълника.
Ако пък вземем за пример квадрата, който е приблизително 4 пъти по-голям от текстурата, координатите на текстурата може да са съответно ( 0, 0 ) , ( 2, 0 ) , ( 2, 2 ) и ( 0, 2 ). Това сигурно ви се струва странно, като се има предвид че цифрата 1 съответства на краен пиксел от текстурата по x или y. Следователно тези координати не могат да бъдат само от една текстурна единица. И точно това е целта. В конкретния случай квадрата ще се покрие от четири отделни копия на нашата текстура. Координати от рода на ( 0, 0 ) , ( 10, 0 ) , ( 10, 10 ) и ( 0, 10 ) ще доведат до покриването на квадрата със 100 текстурни единици.

Само информативно ще спомена, че функцията glTexCoord( ) има доста варианти и някои от тях приемат до 4 ! параметара за координатите на текстурата, включително и указател към масив със съхранените координати. Засега обаче не ви трябва повече от x и y координата т.е. 2 параметъра.

Надявам се че сте разбрали как да определяте координатите на текстурата. Но я си помислете как ще определите координатите на някой от готовите обекти създадени с библиотеките GLAUX, GLUT или GLU ( релеф и квадратични обекти ), като тези обекти се състоят примерно от най-малко 50 полигона, а и нямате пряк достъп до върховете на обекта, за да използвате glTexCoord*( ). И сега идва на помощ тежката артилерия, а това е автоматичното определяне на координатите за дадена текстура върху някой обект. То върши почти перфектно своята работа, използвайки множество текстурни единици за по-големите обекти и предлагащо някои яки ефекти. Единствения недостатък е, че се бъгва на най-простите обекти - например ако си направите куб, само някои от страните му ще са текстурирани правилно. Затова ви представих ръчното определяне на текстурните координати, което ще ви бъде от полза именно в такива моменти. За да включите автоматичното генерирането на координати трябва да подадете на функцията glEnable( ) една от константите GL_TEXTURE_GEN_S,GL_TEXTURE_GEN_T, GL_TEXTURE_GEN_R, или GL_TEXTURE_GEN_Q. На нас ще ни трябват само първите две, които отговарят на генериране на координати съответно по x и y, а това е достатъчно. След като включихте автоматичното генериране идва ред на функцията glTexGen*( ), която определя начина на генериранe на текстурните координати :

void glTexGen{ifd}( GLenum coord, GLenum pname, GLdouble param ) - функцията приема 3 аргумента. Първия може да е една от константите GL_S, GL_T, GL_R или GL_Q. Засега GL_R и GL_Q не ни трябват, че дори за в бъдеще :), а GL_S и GL_T указват изчисляване на текстурните координати съответно по x и y. Tрябва да определите начин на изчисляване на координатите както за x така и за y. Вторият аргумент трябва да е GL_ТЕXTURE_GEN_MODE, a третият може да бъде един от следните :

GL_OBJECT_LINEAR - текстурната единица/и се наслагва по обекта.
GL_EYE_LINEAR - текстурата единица/и не залепва върху повърхността на обекта, а просто се плъзга по него в зависимост от неговото движение/деформации. Така може да симулирате вода с вълни, ако има разбира се повърхност под текстурата, която да се деформира, образувайки вълните.
GL_SPHERE_MAP - текстурата се наслагва по обекта като отражение. Ако искате да си направите сфера, която да рефлектира на околната среда - това е лесния начин.
Ето какво трябва да направите за да включите автоматичното определяне на текстурните координати:


// Включваме автоматично определяне на текстурните координати

glEnable( GL_TEXTURE_GEN_S );

glEnable( GL_TEXTURE_GEN_T );


// Казваме на OpenGL, че искаме нашата текстура просто да се залепи за обекта

glTexGeni( GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR );

glTexGeni( GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR );



Вече сме готови с текстурите, но преди да завърша ще ви кажа още няколко неща за функцията glTexParameter*( ). Както вече знаете първата константа трябва да е GL_TEXTURE_2D за двуизмерни текстури, но за втора константа освен GL_TEXTURE_MIN_FILTER и GL_TEXTURE_MAG_FILTER, могат да се подадат още GL_TEXTURE_WRAP_S и GL_TEXTURE_WRAP_T, които контролират текстурните координати по x или y на множеството текстурни единици върху дадена повърхност, когато има такива разбира се. Когато включите някоя от тях третия ви аргумент трябва да бъде GL_REPEAT или GL_CLAMP. Ако изберете GL_REPEAT и за двете, няма да видите разлика върху стената, в чиято облицовка участват 4 текстури, но при избор на GL_CLAMP, това ще доведе до размножаване на крайните пиксели от първата текстура надясно и нагоре т.е. генерират се координати само по x и само по y, с изключение на първоначалната текстура. Ако пък изберете GL_REPEAT за GL_TEXTURE_WRAP_S и GL_CLAMP за GL_TEXTURE_WRAP_Т, ще получите правилни текстури по дължина ( т.е. долните две ), но по височина ( на мястото на горната двойка текстури) ще са се размножили крайните, горни пиксели от текстурите от първи ред.
И последно, ако искате да имате рамка на текстурата трябва да извикате функцията glTexParameterfv( ), като подадете за аргументи съответно константите GL_TEXTURE_2D, GL_TEXTURE_BORDER и масив от тип float с RGB стойностите, определящи цвета на рамката.


Texture матрица

Сега ще си позволя ( тази тема стана малко дълга вече ) да представя още няколко неща свързани с текстурите, но мисля че ще ви бъдат от полза. Може би се досещате, чe съществуваше и GL_TEXTURE матрица, или не? С нейна помощ може да постигнете особено добри ефекти. Тази матрица манипулира самите текстури. Можете да ползвате вече познатите ви glRotate*( ) и glTranslate*( ), както и glPushMatrix( ) glPopMatrix(). Сега ще си помислете : "Защо ще ми трябва да движа текстурата по обекта или да я въртя ?!?". Ами ако искате да направите реалистично небе това ще ви свърши чудесна работа. Ще ви покажа едно парче сорс, което ще ви помогне да разберете нещата. След като сте изчистили буферите и сте извикали функцията glLoadIdentity( ) за Modelview матрицата във вашата рендерираща функция можете да включите успешно TEXTURE матрицата:


glMatrixMode( GL_TEXTURE );

// След това трябва да извикате glLoadIdentity( ) за да включите първоначалната TEXTURE матрица

glLoadIdentity( );

/* Следва манипулацията, която искате да направите. С glRotate*( ) и завъртане по z се постигат най - добри резултати т.е. текстурата наистина се върти, докато при x и y наблюдаваме удължаване и свиване на текстурата. Не забравяйте, че манипулираме текстура, а не 3D обект, така че очаквайте такива аномалии.
При glTranslate*( ) трансформацията е по x или y е просто движение на текстурата по обекта, докато при z координатата не се случва нищо. Но общо казано използвайте само glRotate*( ) с изменение по z, aко искате да правите нормално небе или море. */

glRotatef( angle , 0.0 , 0.0 , 1.0 );

// Сега трябва да превключим на Modelview матрица за да създадем самото небе, което ще е един квадрат

glMatrixMode( GL_MODELVIEW );

// Свързваме текстурата за небето, която предварително трябва да сме построили

glBindTexture( GL_TEXTURE_2D , clouds[0] );

// Изграждаме квадрата и определяме текстурните координати за всеки връх

glBegin(GL_QUADS);

glTexCoord2f( 0.0 , 0.0 ); glVertex3f( 25.0f , 3.0f, 50.0 );

glTexCoord2f( 1.0 , 0.0 ); glVertex3f( -25.0f , 3.0f, 50.0 );

glTexCoord2f( 1.0 , 1.0 ); glVertex3f( -25.0f , 3.0f, -50.0 );

glTexCoord2f( 0.0 , 1.0 ); glVertex3f( 25.0f , 3.0f, -50.0 );

glEnd();


/* Сега идва един много особен момент, а това е че промяната на TEXTURE матрицата се отразява на всички текстури, а ние искаме единствено да манипулираме текстурата на квадрата-небе. Затова трябва да включим отново TEXTURE матрицата и да заредим първоначалната TEXTURE матрица. */


glMatrixMode( GL_TEXTURE );

glLoadIdentity();

// Зареждаме отново Modelview матрицата за да построим останалите обекти от сцената, ако има такива

glMatrixMode( GL_MODELVIEW );


Безспорно - доста добър ефект и то лесно постижим.
Е това беше наистина всичко, което исках да ви представя. Надявам се да сте разбрали текстурирането, защото това е наистина нещо важно. Сигурен съм, че все още имате поне няколко въпроса. Ето тук може да видите един добър сорс код използващ текстуриране, с възможност за смяна режима на филтриране в реално време.

Автор: Иван Георгиев Иванов [ Nickname: tuschko ]
e-mail: tuschko@abv.bg


Този сайт е хостван от сървър на
Headoff Gaming Intranetwork