В этом уроке я расскажу вам, как использовать массивы вершин для ускорения рендеринга, и что это вообще такое. Массивы вершин позволяют передавать OpenGL данные в форме массивов, которые рендерятся непосредственно на видеокарте из массива.
Это позволяет не строить длинные циклы, которые значительно замедляют процесс рендеринга.

Например, скажем у вас есть большой обьект, содержащий тысячи полигонов.
Обычно вы построили бы большой цикл, проходящий через каждую вершину, и рендерили бы их с помощью glBegin() и glVertex3f(), верно?
С массивами вершин вместо тысяч вызовов glVertex3f() мы можем сделать только ОДИН вызов, что-нибуть вроде glDrawArrays(…). Уже заинтересовались? 😉 Это необходимо использовать в каждом более-менее крупном 3д проекте. Если вы посмотрите исходники любой большой игры, например 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().
Далее передаём в pointer-функции тип данных (float), передаем 0 для «шага данных».
Потому что у нас массив с только одним типом данных. У нас нет комплексных структур (как CVertex), хранящих несколько типов данных. Если они у нас будут, нам нужно будет передать sizeof() типа данных, чтобы OpenGL знало, как извлечь данные текстуры/вершины/нормали.
Для примера смотрите применение нашего glInterleavedArrays(), или загрузчик Quake BSP.

Давайте рассмотрим функции работы с массивами вершин:

* glDrawArrays() *

Это, наверное, самый популярный способ использования массивов вершин. Единственное, что вы передаёте — тип данных (треугольники и т.д…), стартовый индекс массива и количество элементов массива для отображения: glDrawArrays(GL_TRIANLGES, 0, 9);

Этот код отрисует треугольники, начиная с индекса 0 до 9. Запомните и не путайте: этот код отрендерит 9 вершин, а не треугольников.

* glDrawElements() *

Эта функция немного сложнее, но и более полезная, если ваши данные содержат обьекты, имеющие face-ы. Например, когда вы загружаете 3д файл, вам нужно не просто загрузить и отрендерить вершины из файла. Вам нужно создать структуру «face», которая содержит номера вершин для её создания. Эта функция позволяет передать OpenGL индексы, указывающие, в каком порядке рендерить вершины. Рассмотрим пример:
glDrawElements(GL_TRIANGLES, pObject->m_numberOfFaces * 3, GL_UNSIGNED_INT, pObject.m_pIndices);

Как вы можете видеть, vы передали ТИП (GL_TRIANGLES), количество вершин для отображения (номер полигонов * 3 (если это треугольники)), потом тип переменных (GL_UNSIGNED_INT), и наконец массив индексов (фейсов).

Единственный минус — когда мы загружаем 3д обьект, обычно в файле больше текстурных координат и/или нормалей, чем вершин. Но вы не можете указать разные массивы для разных типов данных (вершина, текст. координаты, и т.д.). Это расплата за рендер всей информации с использованием массивов вершин. Так что вершины отрисуются прекрасно, но текстуры будут выглядеть забавно.

Чтобы обойти эту ситуацию, у вас есть две возможности.
1) Первая — пройтись циклом и просто дублировать вершинные данные и поместить их в структуру типа CVertex (см. ниже в уроке), которая содержит вершину, текстурные координаты и нормали. Это, конечно, увеличит использование памяти, но при долгой работе (если возросшая память не проблема для вашего компьютера) рендер будет быстрее.

2) Второй метод — создать структуру, содержащую эту информацию, но установить ваши данные так, чтобы рендер проходил с использованием «шагов треугольника».
Много игр это делают, и результат всегда быстр. Вы можете посмотреть урок «загрузка Quake BSP», чтобы увидеть этот процесс в действии.

* glInterleavedArrays() *

Эта функция может ввести в заблуждение. На самом деле она ничего не рендерит, но позволяет вам установить, что именно вы хотите рисовать. Имеется ввиду, что вместо вызовов glEnableClientState() и gl***Pointer(), вы просто передаете тип данных, которые хотите рендерить (будь это текстурные координаты, вершины, цвета или нормали). Вот пример применения:
glInterleavedArrays(GL_T2F_V3F, sizeof(CVertex), &g;_InterleacedVertices);

Этим мы говорим OpenGL, что у нас есть структура, содержащая данные текстуры и вершин (GL_T2F_V3F). Также мы сообщаем OpenGL размер структуры данных. Наконец, мы передаём OpenGL указатель на данные (массив), которые содержат и текстурные и вершинные координаты.

// * ЗАПОМНИТЕ * вам необходимо убедиться, что вы заполнили вашу структуру (или класс) данными
// в том же порядке, какой вы указали OpenGL. Указывая «GL_T2F_V3F», мы говорим OpenGL,
// что данные текстуры идут перед данными вершин. В своем классе/структуре убедитесь,
// что вы расположили данные точно так же (см. CVertex). Если это не так, работать программа
// не будет или скорее будет, но криво.

Есть много других констант для чередованных (interleaved) массивов:
GL_V3F, GL_N3F_V3F, GL_T2F_C4UB_V3F,
GL_T2F_C4F_N3F_V3F, и т.д…

Для всех возможных определений см. MDSN. Итак, вызывая эту ф-ю, мы ничего не рендерим, для рендера нам нужно вызвать что-то вроде glDrawArrays(). Данные в действительности рендерит именно оно, а не glInterleavedArrays().

* glArrayElement() *

Наконец, самая простая функция — 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].x = 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 полигона содержат одну и ту же вершину, они оба указывают её индекс, а сама вершина не сохраняется дважды.

Итак, вернёмся к коду:

// Вверху файла main.cpp добавим структуру CFace:
struct CFace{
int v1,v2,v3;
};// Теперь обьявим данные для новой фигуры.
// Мы нарисуем квадрат. Используя glDrawArrays(), пришлось бы создать 6 вершин
// (по 3 на треугольник), но с glDrawElements() нам понадобятся только 4 вершины и 2 полигона:
CFace f[2];
CVertex3 vQuad[4];// В функции init() после инициализации вершин треугольника инициализируем данные квадрата:

vQuad[0].x = 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].x = 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;
};CTexmap tex[4];     // 4 координаты для четырёх вершин// Ещё добавим структуру для вершин второго квадрата:
CVertex3 vQuad2[4];

/////////////////////////////////////////////////////////
//
// В код init.cpp() добавим, во-первых, загрузку текстуры:
Texture->LoadTexture(IL_JPG,«image.jpg»,&textures[0]);

// Во-вторых, заполним структуру текстурных координат:
tex[0].u = 0;   tex[1].u = 1;   tex[2].u = 1;   tex[3].u = 0;
tex[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;

////////////////////////////////////////////////////////
//
// Наконец, переходим к RenderScene().
// Добавьте в функцию следующий код:

// Загружаем нашу текстуру:
glBindTexture(GL_TEXTURE_2D, textures[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);