Текстурни масиви (върхови масиви), уроци по програмиране и примери
Например, да кажем, че имате голям обект, съдържащ хиляди многоъгълници. Обикновено бихте изградили голям цикъл през всеки връх и ги рендирате с glBegin() и glVertex3f(), нали? С върховите масиви, вместо хиляди извиквания на glVertex3f(), можем да направим само ЕДНО извикване, нещо като glDrawArrays(. ). Вече се интересувате? ;) Необходимо е да се използва във всеки повече или по-малко голям 3d проект. Ако погледнете източника на някоя голяма игра като quake, ще видите, че всички те използват върхови масиви. Игрите се нуждаят от всяко минимално увеличение на производителността, тъй като данните за играта са много големи и използването на масиви от върхове дава доста голяма печалба.
Използването на масиви от върхове ще ускори програмата с 10 или дори 20 пъти, в зависимост от това колко върхове начертаете. Разбира се, в този урок просто ще начертаем няколко триъгълника, така че няма да забележите голяма разлика, но исках да покажа _прост пример_, показващ повечето от начините, по които може да се използват върховите масиви. Добавете още 5000 триъгълника и усетете разликата ;) Ето функциите, които манипулират масивите от върхове:
glDrawArrays(), glDrawElements(), glInterleavedArrays(), glArrayElement()
* Инициализация (настройка) на масиви *
Преди да се изгубите в кода, нека набързо прегледаме процеса на "задаване" на масиви от върхове. Ще ви трябват някои от тези функции (в зависимост от какво и как искате да изобразите):
glEnableClientState(.); glTexCoordPointer(. ); glVertexPointer(. ); glNormalPointer(. );
glEnableClientState() приема 1 параметър, за да зададе кой тип масив искате да използвате. Ако искашза изобразяване на масиви от върхове с координати на текстура, трябва да подадете GL_TEXTURE_COORD_ARRAY, GL_VERTEX_ARRAY за върхове и GL_NORMAL_ARRAY за нормали. Разгледайте MDSN (mdsn.microsoft.com) за повече дефиниции (цвят и т.н.). Подминавате само 1 наведнъж, защото сте след това извиквате функцията, от която се нуждаете - gl***Pointer(), за да кажете на OpenGL от кой масив се предават данните. Ето един прост пример:
glEnableClientState(GL_TEXTURE_COORD_ARRAY); // Предаване на масив от координати на текстура: glTexCoordPointer(2, GL_FLOAT, 0, pObject->pTexVerts);
glEnableClientState(GL_VERTEX_ARRAY); // Предаване на масив от геометрични върхове: glVertexPointer(3, GL_FLOAT, 0, pObject->pVerts);
glEnableClientState(GL_NORMAL_ARRAY); // Предаване на масив от нормали: glNormalPointer(GL_FLOAT, 0, pObject->pNormals);
Това насочи OpenGL към нашите данни и какъв тип бяха. Извиквате glEnableClientState() точно като glEnable(). След това предаваме типа данни (float) на функцията-указател, предаваме 0 за „стъпка с данни“, Защото имаме масив само с един тип данни. Нямаме сложни структури (като CVertex), които съхраняват множество типове данни. Ако ги имаме, ще трябва да предадем типа данни sizeof(), така че OpenGL да знае как да извлече данните за текстура/върх/нормални данни. За пример вижте нашето приложение glInterleavedArrays() или програмата за зареждане на Quake BSP.
Нека да разгледаме функциите за работа с масиви от върхове:
Това е може би най-популярният начин за използване на масиви от върхове. Единствените неща, които предавате в , са типът данни (триъгълници и т.н.), началният индекс на масива и броят на елементите на масива за показване:
glDrawArrays(GL_TRIANLGES, 0, 9);
Товакодът ще рендира триъгълници, започващи от индекс 0 до 9. Запомнете, не бъркайте : този код ще рендира 9 върха, а не триъгълници.
Тази функция е малко по-сложна, но и по-полезна, ако вашите данни съдържат обекти, които имат лица. Например, когато заредите 3d файл, не е нужно просто да заредите и да изобразите върховете от файла. Трябва да създадете структура "лице", която съдържа броя върхове, за да я създадете. Тази функция ви позволява да предавате индекси на OpenGL, указващи в какъв ред да се визуализират върховете. Помислете за пример:
glDrawElements(GL_TRIANGLES, pObject->m_numberOfFaces * 3, GL_UNSIGNED_INT, pObject.m_pIndices);
Както можете да видите, сте предали TYPE (GL_TRIANGLES), броя на върховете за показване (брой полигони * 3 (ако са триъгълници)), след това типа на променливата (GL_UNSIGNED_INT), и накрая масива от индекси (лица).
Единственият недостатък е, че когато заредим 3d обект, обикновено във файла има повече текстурни координати и/или нормали, отколкото върхове. Но не можете да посочите различни масиви за различни типове данни (връх, text.coordinates и т.н.). Това е цената за изобразяване на цялата информация с помощта на масиви от върхове. Така че върховете ще се рендират добре но текстурите ще изглеждат смешно. За да преодолеете тази ситуация, имате две възможности. 1) Първият е да превъртите и просто да дублирате данните за върха и да го поставите в CVertex структура (вижте по-нататък в урока), която съдържа върха, координатите на текстурата и нормалите. Това разбира се ще увеличи използването на паметта, но в дългосрочен план (ако увеличената памет не е проблем за вашия компютър) изобразяването ще бъде по-бързо. 2) Вторият метод е да създадете структура, съдържаща тази информация, но задайте вашите данни така, че да се изобразяват сс помощта на триъгълни стъпки. Много игри правят това и резултатът винаги е бърз. Можете да гледате урок „Изтегляне на Quake BSP“, за да видите този процес в действие.
Тази функция може да бъде объркваща. Всъщност не изобразява нищо , но ви позволява да зададете какво искате да нарисувате. Това означава, че , вместо да извиквате glEnableClientState() и gl***Pointer(), вие просто предавате типа данни, които искате да изобразите (било то текстурни координати, върхове, цветове или нормали). Ето пример за приложение:
Това казва на OpenGL, че имаме структура, съдържаща данни за текстура и връх (GL_T2F_V3F). Също така казваме на OpenGL размера на структурата от данни. Накрая предаваме OpenGL указател към данни (масив), който съдържа както текстура, така и координати на върха.
// * ЗАПОМНЕТЕ * трябва да се уверите, че попълвате вашата структура (или клас) с данни // в същия ред, както сте посочили за OpenGL. Като посочим "GL_T2F_V3F", казваме на OpenGL //, че данните за текстурата идват преди данните за върха. Във вашия клас/структура се уверете, че // оформяте данните по същия начин (вижте CVertex). Ако това не е така, програмата // няма да работи или по-скоро ще работи, но ще бъде крива.
Има много други константи за редуващи се масиви: GL_V3F, GL_N3F_V3F, GL_T2F_C4UB_V3F, GL_T2F_C4F_N3F_V3F и др. Вижте MDSN за всички възможни определения. Така че, извиквайки тази функция, ние не изобразяваме нищо, за да изобразим трябва да извикаме нещо като glDrawArrays(). Това е , което всъщност изобразява данните, а не glInterleavedArrays().
И накрая, най-простата функция е glArrayElement(). Той изобразява само ЕДИН елемент от масива , предаден по-рано на OpenGL. Може би си мислите, че няма разликамежду това и просто изобразяване на без никакви масиви. Но тук вие извиквате само една функция за един връх, вместо да извиквате много функции, когато извеждате текстура и нормална информация. Може да ви спести много код. Трябва да използвате glArrayElement() вътре в glBegin() и glEnd().
glBegin(GL_TRIANGLES); glArrayElement(0); glArrayElement(1); glArrayElement(2); glEnd();
Този код просто рисува триъгълник от върхове с индекси 1,2 и 3.
* И така, какво е показано в този урок? *
В този урок ние просто ще покажем куп триъгълници, използвайки различни методи за масив от върхове, за да покажем най-простия възможен пример за работа с тях.
Изходните кодове са взети от урока "Зареждане на текстури". И така, нека да започнем. Променяме само файла main.cpp:
// Първо, нека да разгледаме пример за работа с glDrawArrays(). // След включванията в горната част на файла, декларирайте масив от върхове за триъгълника: CVertex3 v [ 3 ] ;
// Във функцията Init() инициализирайте тези върхове. Разбира се, в реални проекти това // ще бъде направено от подсистемата, която зарежда моделите - нереалистично е ръчно да напишете всеки връх // в голям проект =) v [ 0 ] . x = - 2.0f; v [ 1 ] . х = 0.0f v [ 2 ] . x = - 1.0f; v [ 0 ] . y = 0.0f v [ 1 ] . y = 0.0f v [ 2 ] . y=1.5f v [ 0 ] . z = 0.0f v [ 1 ] . z = 0.0f v [ 2 ] . z = 0.0f
// Отидете до функцията RenderScene(). // Първо се върнете малко назад: glTranslatef ( 0.0f , 0.0f ,- 5.0f ) ;
// И след това помолете OpenGL да изобрази формите от масива: glEnableClientState ( GL_VERTEX_ARRAY ) ; // Активиране на режима на масив от върхове. // Направете това само веднъж (или ако го изключим по-късно), точно като glEnable().
// Посочете изходния масив на OpenGL.Предаваме броя на координатите за всяка // точка (x,y,z), тип данни (float), размер на структурата и самата структура. glVertexPointer (3, GL_FLOAT, sizeof (CVertex3), v);
// И начертайте три елемента от масива, започвайки от нула. glDrawArrays (GL_TRIANGLES, 0, 3); glDisableClientState (GL_VERTEX_ARRAY); // И накрая изключете режима на масив от върхове.
Всъщност – съвсем просто, въпреки пространните обяснения по-горе, нали? =)
Сега нека се опитаме да начертаем квадрат с glDrawElements(). Например, трябва да начертаете квадрат, състоящ се от два триъгълника. Използвайки glDrawArrays() вие предавате масив от координати в реда те ще бъдат начертани. И ако има върхове в сцената, които принадлежат към два и повече полигона наведнъж, те трябва да бъдат дублирани в масива, следователно размерът му ще се увеличи значително . Използвайки glDrawElements(), в допълнение към масива от върхове, предавате масив от лица (многоъгълници), който съдържа индексите на текстурите, които ги основават. Тоест, ако 2 полигона съдържат един и същ връх, и двата посочват неговия индекс, а самият връх не се записва два пъти.
И така, обратно към кода:
// Добавяне на CFace структура в горната част на файла main.cpp: struct CFace < int v1, v2, v3; > ;
// Сега нека декларираме данните за новата фигура. // Ще начертаем квадрат. Използвайки glDrawArrays() ще трябва да създадем 6 // върха (3 на триъгълник), но с glDrawElements() имаме нужда само от 4 върха и 2 полигона: CFace f [ 2 ] ; CVertex3 vQuad [4];
// Във функцията init(), след инициализиране на върховете на триъгълника, инициализираме квадратните данни:
vQuad [ 0 ] . х = 0.0f vQuad [ 1 ] . x = 2.0f vQuad [ 2 ] . x = 2.0f vQuad [0] . y = 0.0f vQuad [ 1 ]. y = 0.0f vQuad [ 2 ] . y=2.0f vQuad [0] . z = 0.0f vQuad [ 1 ] . z = 0.0f vQuad [ 2 ] . z = 0.0f
vQuad [ 3 ] . х = 0.0f vQuad [ 3 ] . y=2.0f vQuad [ 3 ] . z = 0.0f
// Дефиниране на лицата // Определяне от кои върхове се състоят, в ред обратен на часовниковата стрелка f [ 0 ] . v1 = 0; f [ 0 ] . v2 = 1; f [ 0 ] . v3 = 2; // Първи триъгълник f [ 1 ] . v1 = 2; f [ 1 ] . v2 = 3; f [ 1 ] . v3 = 0; // Втори триъгълник
// Надявам се, че разбирате какво означава всичко това. Уточнихме, че многоъгълник #1 (f[0]) // е върхове 0,1 и 2, а полигон #2 (f[1]) е върхове 2,3 и 0. // Както можете да видите, върховете "0" и "2" принадлежат на двата триъгълника (многоъгълника), // но не трябваше да ги повтаряме два пъти.
// Отидете до функцията RenderScene(). // Добавете квадратния код под кода за показване на триъгълник:
glEnableClientState (GL_VERTEX_ARRAY); glVertexPointer (3, GL_FLOAT, sizeof (CVertex3), vQuad); // Посочете масив от върхове.
// Подаване на масив от индекси на върхове. // Първият параметър е типът на примитивите (имаме 2 триъгълника), вторият - // броят на полигоните * броят на върховете в примитивите. В нашия случай - // 2 полигона * 3 точки. GL_QUADS ще има 2*4 и т.н. // След това предаваме типа променлива (GL_UNSIGNED_INT) и накрая масив от индекси // върхове (многоъгълници. glDrawElements (GL_TRIANGLES, 6, GL_UNSIGNED_INT, f) ; glDisableClientState (GL_VERTEX_ARRAY) ;
Нищо сложно също, нали? Но колко полезно!
Какво друго? Нека сега се опитаме да приложим текстурни координати към форма от масив от върхове:
// И така, в началото на файла, на същото място като останалите декларации, добавете нова структура и нейния // екземпляр: // В началотодобавете текстурен координатен клас: struct CTexmap < // Структура за съхраняване на текстурни координати float u, v; > ;
CTexmaptex[4]; // 4 координати за четири върха
// Добавяне на друга структура за върховете на втория квадрат: CVertex3 vQuad2 [ 4 ] ;
// Второ, попълнете координатната структура на текстурата: tex [ 0 ] . u = 0 tex[1] . u = 1; tex [ 2 ] . u = 1; tex[3] . u = 0 текс [0] . v = 0 tex[1] . v = 0 tex [ 2 ] . v = 1; tex[3] . v = 1;
// И попълнете върховете на втория квадрат: vQuad [ 0 ] . x = - 1.0f; vQuad [ 1 ] . x = 1.0f vQuad [ 2 ] . x = 1.0f vQuad [0] . y=-2.5f vQuad [ 1 ] . y=-2.5f vQuad [ 2 ] . y=-0.5f vQuad [0] . z = 0.0f vQuad [ 1 ] . z = 0.0f vQuad [ 2 ] . z = 0.0f
vQuad [ 3 ] . x = - 1.0f; vQuad [ 3 ] . y=-0.5f vQuad [ 3 ] . z = 0.0f
// Зареждане на нашата текстура: glBindTexture ( GL_TEXTURE_2D , текстури [ 0 ] . texID ) ;
// Разрешаване на масиви от върхове: glEnableClientState ( GL_VERTEX_ARRAY ) ; // Разрешаване на масиви от текстурни координати: glEnableClientState ( GL_TEXTURE_COORD_ARRAY ) ; // Посочете масив от върхове, както направихме преди: glVertexPointer ( 3 , GL_FLOAT , sizeof ( CVertex3 ) , vQuad2 ) ; // По същия начин, но предавайки данните за текстурата, ние указваме координатите на текстурата: glTexCoordPointer ( 2 , GL_FLOAT , sizeof ( CTexmap ) , tex ) ; // И накрая, рисуваме елементите. Многоъгълниците // на втория квадрат използват същия брой върхове като тези на първия, така че масивът f[] // също ще бъде указан тук: glDrawElements ( GL_TRIANGLES , 6 , GL_UNSIGNED_INT , f ) ; glDisableClientState (GL_VERTEX_ARRAY);