NeHe Tutorials Народный учебник по OpenGL
Урок 45. OpenGL

Vertex Buffer Objects

 

Одна из главных целей любого трехмерного приложения – скорость. Всегда есть ограничения на число полигонов, которые можно визуализировать, не важно используете ли вы сортировку, отсечение, или алгоритмы детализации. Однако когда все эти ухищрения терпят не удачу, вам все еще надо мощь для проталкивания полигонов по графическому конвейеру, вы можете использовать возможности оптимизации встроенные в OpenGL. Вершинные массивы (vertex arrays) один из тех хороших способов, плюс есть еще вершинные буфера (Vertex Buffer Objects - VBO), которые увеличат FPS, для тех, кто этого хочет. Расширение ARB_vertex_buffer_object, работает так же как вершинные массивы, исключая повторную загрузку данных в память графической карты, заметно понижая время визуализации. Конечно, это расширение будет работать не на всех видеокартах, но со временем, оно станет обязательным.

 

В этом уроке мы будем:

·       Загружаться данные для карты высот

·       Использоваться вершинные массивы, для того чтобы послать данные в OpenGL наиболее эффективно

·       Загрузить данные в быстродействующую память видеокарты через расширение VBO

 

Давайте начнем! Вначале зададим несколько параметров.

 

#define MESH_RESOLUTION 4.0f // Пикселей на вершину

#define MESH_HEIGHTSCALE 1.0f // Масштабирование высоты сетки

//#define NO_VBOS             // Если задано, VBO будут отключены

 

Первые две константы - стандартный набор для создания карты высот. Вначале идет константа разрешения в пикселях, с которым будет сгенерирована карта высот, затем константа вертикального масштабирования карты высот. Третья константа, когда задана, будет выключать VBO, чтобы можно было ощутить разницу.

 

Затем мы задаем данные для работы с расширением VBO: константы, тип данных, и указатели на функции.

 

// Определение расширения VBO, из glext.h

#define GL_ARRAY_BUFFER_ARB 0x8892

#define GL_STATIC_DRAW_ARB 0x88E4

typedef void (APIENTRY * PFNGLBINDBUFFERARBPROC) (GLenum target, GLuint buffer);

typedef void (APIENTRY * PFNGLDELETEBUFFERSARBPROC) (GLsizei n, const GLuint *buffers);

typedef void (APIENTRY * PFNGLGENBUFFERSARBPROC) (GLsizei n, GLuint *buffers);

typedef void (APIENTRY * PFNGLBUFFERDATAARBPROC) (GLenum target, int size, const GLvoid *data, GLenum usage);

 

// Указатели на функции VBO расширения

PFNGLGENBUFFERSARBPROC glGenBuffersARB = NULL; // VBO Name Generation Procedure

PFNGLBINDBUFFERARBPROC glBindBufferARB = NULL; // VBO Bind Procedure

PFNGLBUFFERDATAARBPROC glBufferDataARB = NULL; // VBO Data Loading Procedure

PFNGLDELETEBUFFERSARBPROC glDeleteBuffersARB = NULL;     

 

Я поместил в текст, только те определения, которые необходимы для работы программы. Если вы хотите иметь все возможности расширения VBO, я рекомендую использовать последнюю версию glext.h взятую с http://www.opengl.org.

 

Сейчас пойдут стандартные математические определения, плюс класс сетки. Все это составляет костяк для программы урока. Как всегда, я рекомендую разработать свою собственную математическую библиотеку.

 

class CVert                  // Класс Вершины

{

public:

  float x;                // X компонента

  float y;                // Y компонента

  float z;                // Z компонента

};

typedef CVert CVec;                // Определение синоним

 

class CTexCoord                  // Класс Текстурной координаты

{

public:

  float u;                // U компонента

  float v;                // V компонента

};

 

class CMesh

{

public:

  // Данные сетки

  int    m_nVertexCount;            // Число вершин

  CVert*    m_pVertices;            // данные вершин

  CTexCoordm_pTexCoords;            // Текстурные координаты

  unsigned int  m_nTextureId;            // Идентификатор текстуры

 

  // Имена для вершинного буфера

  unsigned int  m_nVBOVertices;            // Имя VBO

  unsigned int  m_nVBOTexCoords;          // Имя координат текстуры VBO

 

  // Временные данные

  AUX_RGBImageRec* m_pTextureImage;          // Данные карты высот

 

public:

  CMesh();                // Конструктор сетки

  ~CMesh();                // Деструктор сетки

 

  // Загрузка карты высот

  bool LoadHeightmap( char* szPath, float flHeightScale, float flResolution );

  // Высота заданной точки

  float PtHeight( int nX, int nY );

  // Функция построения VBO

  void BuildVBOs();

};

 

Большинство кода очень понятно. Замечу, что я сохраняю координаты вершин и текстур раздельно, что не совсем необходимо, как будет отмечено далее.

 

Далее определим глобальные переменные. Вначале флаг, того, что VBO расширение определено, который будет вычислен в коде инициализации. Далее зададим нашу сетку, затем счетчик вращений. В конце идут переменная для отображения FPS. Я решил отображать FPS, чтобы показать насколько влияет оптимизация в этом коде.

 

bool    g_fVBOSupported = false; // есть поддержка ARB_vertex_buffer_object?

CMesh*    g_pMesh = NULL;        // Данные сетки

float    g_flYRot = 0.0f;        // Вращение

int    g_nFPS = 0, g_nFrames = 0; // FPS и счетчик FPS

DWORD    g_dwLastFPS = 0;        // Последняя проверка FPS

 

Давайте опустим функции CMesh, и начнем с LoadHeightmap. Напомню, что карты высот – это двухмерный массив, похожий на изображение, которое задает сетки ландшафта. Есть много путей реализации карты высот, и конечно, нет одного правильного пути. В моей реализации читаются три канала побитной карты, и используется алгоритм определения яркости для определения высоты из данных. В результате данные будут точно такими же, как если бы изображение было в цвете или в градациях серого, которое позволяет карте высот быть в цвете. Сам бы я рекомендовал использовать четырех канальное изображение, такое как targa, и использовать альфа канал для высоты. Однако, для использования в это уроке, я решил, что использовать простую побитную карту будет лучше

 

Вначале, мы проверим, что карта высот существует, если это так, мы используем загрузчик изображений GLaux. Да, да, возможно лучше иметь свой собственный загрузчик, но это не относиться к этому уроку.

 

bool CMesh :: LoadHeightmap( char* szPath, float flHeightScale, float flResolution )

{

  // Проверка на ошибку

  FILE* fTest = fopen( szPath, "r" ); // Открыть изображение

  if( !fTest )                // Проверим найдено?

    return false;             // Если нет, файл тоже нет

  fclose( fTest );            // Закрыть файл

 

  // Загрузка данных текстуры

  m_pTextureImage = auxDIBImageLoad( szPath );        // Используем процедуру GLaux

 

Далее более интересно. Прежде всего, я хотел бы указать на то, что в моей карте высот генерируется три вершины на один треугольник, вершины не объединяются. Далее я поясню почему, но я полагаю, что вы должны это знать, прежде чем вы увидите код.

 

Я начинаю вычислять число вершин в сетке. Вот формула: ((Ширина ландшафта / Разрешение)* (Длина ландшафта / разрешение) * 3 Вершины на треугольник * 2 треугольника на квадрат). Затем я располагаю мои данные и начинаю работу по вычислению вершин.

 

  // генерировать поле вершин

  m_nVertexCount = (int) ( m_pTextureImage->sizeX * m_pTextureImage->sizeY * 6 /

   ( flResolution * flResolution ) );

  m_pVertices = new CVec[m_nVertexCount]; // Выделить данные для вершин

  m_pTexCoords = new CTexCoord[m_nVertexCount]; // Выделить данные для текстурных координат

  int nX, nZ, nTri, nIndex=0; // Создать переменные

  float flX, flZ;

  for( nZ = 0; nZ < m_pTextureImage->sizeY; nZ += (int) flResolution )

  {

    for( nX = 0; nX < m_pTextureImage->sizeX; nX += (int) flResolution )

    {

      for( nTri = 0; nTri < 6; nTri++ )

      {

        // Использовать быстрый хак, вычислить X,Z позиции точки

        flX = (float) nX + ( ( nTri == 1 || nTri == 2 || nTri == 5 ) ? flResolution : 0.0f );

        flZ = (float) nZ + ( ( nTri == 2 || nTri == 4 || nTri == 5 ) ? flResolution : 0.0f );

 

        // Задать данные, используя PtHeight для получения Y значения

        m_pVertices[nIndex].x = flX - ( m_pTextureImage->sizeX / 2 );

        m_pVertices[nIndex].y = PtHeight( (int) flX, (int) flZ ) *  flHeightScale;

        m_pVertices[nIndex].z = flZ - ( m_pTextureImage->sizeY / 2 );

 

        // растянуть текстуру вдоль всей сетки

        m_pTexCoords[nIndex].u = flX / m_pTextureImage->sizeX;

        m_pTexCoords[nIndex].v = flZ / m_pTextureImage->sizeY;

 

        // Увеличить индекс

        nIndex++;

      }

    }

  }

 

Я завершаю функцию загрузкой текстуры в OpenGL, и освобождая данные. Это знакомо по прошлым урокам.

 

  // Загрузить текстуры в OpenGL

  glGenTextures( 1, &m_nTextureId ); // Получить ID

  glBindTexture( GL_TEXTURE_2D, m_nTextureId ); // Привязка текстуры

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

  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);

  glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);

 

  // Освободить данные текстуры

  if( m_pTextureImage )

  {

    if( m_pTextureImage->data )

      free( m_pTextureImage->data );

    free( m_pTextureImage );

  }

  return true;

}

 

float CMesh :: PtHeight( int nX, int nY )

{

  // Вычислить позицию в текстуре, не допуская переполнения

  int nPos = ( ( nX % m_pTextureImage->sizeX )  + ( ( nY % m_pTextureImage->sizeY )

              * m_pTextureImage->sizeX ) ) * 3;

  float flR = (float) m_pTextureImage->data[ nPos ]; // Взять красный

  float flG = (float) m_pTextureImage->data[ nPos + 1 ]; // зеленый

  float flB = (float) m_pTextureImage->data[ nPos + 2 ]; // синий

  return ( 0.299f * flR + 0.587f * flG + 0.114f * flB ); // Вычислить высоту используя алгоритм вычисления яркости

}

 

Ура! Пришло время вершинных массивов и буферов. Так что же такое вершинные массивы? В сущности, это способ передать OpenGL геометрические данные за минимальное число вызовов функций OpenGL. В результате увеличится быстродействие по сравнению с использованием других функций (glVertex). Что же такое вершинные буфера? Для буфера вершин используется скоростная графическая память вместо обычной памяти. Это не только сокращает количество операций на каждый фрейм, но и сокращает дистанцию на шине, по которой путешествуют ваши данные. На моем компе, использование буферов утраивает частоту кадров, что не так мало. (Примечание переводчика: к сожалению, сейчас разница между вершинными массивами и буферами не столь огромна).

 

Поэтому мы будем строить вершинные буфера. Есть пара способов добиться этого, один из которых называется «отображение» в память. Я думаю, что лучший способ состоит вот в чем: первое, использовать glGenBuffersARB для получения «имени» буфера. Имя – это идентификационный номер, который OpenGL ассоциирует с вашими данными. Далее, мы активизируем буфер при помощи связывания glBindBufferARB. В завершении, мы загружаем данные в графическую карту при помощи glBufferDataARB, передавая размер и указатель на данные. glBufferDataARB будет копировать наши данные в память графической карты, поэтому мы сможем их удалить из обычной памяти.

 

void CMesh :: BuildVBOs()

{

  // Генерировать и связать вершинный буфер

  glGenBuffersARB( 1, &m_nVBOVertices ); // Получить правильное имя

  glBindBufferARB( GL_ARRAY_BUFFER_ARB, m_nVBOVertices ); // Связывание

  // Загрузка данных

  glBufferDataARB( GL_ARRAY_BUFFER_ARB, m_nVertexCount*3*sizeof(float), m_pVertices, GL_STATIC_DRAW_ARB );

 

  // Генерировать и связать буфер для текстурных координат

  glGenBuffersARB( 1, &m_nVBOTexCoords ); // Получить правильное имя

  glBindBufferARB( GL_ARRAY_BUFFER_ARB, m_nVBOTexCoords ); // Связывание

  // Загрузка данных

  glBufferDataARB( GL_ARRAY_BUFFER_ARB, m_nVertexCount*2*sizeof(float), m_pTexCoords, GL_STATIC_DRAW_ARB );

 

  // Наши данные скопированы, мы их можем удалить

  delete [] m_pVertices; m_pVertices = NULL;

  delete [] m_pTexCoords; m_pTexCoords = NULL;

}

 

Отлично, пришло время для инициализации. Первое мы выделим и загрузим данные сетки. Затем мы проверим, если ли поддержка  wglGetProcAddress GL_ARB_vertex_buffer_object. Если есть, мы получаем указатель на функцию wglGetProcAddress и построим наш буфер. Отметим, что если буфера не поддерживаются, мы сохраняем данные как обычно. Так же обратите внимание на условие отмены поддержки буферов.

 

  // Загрузка данных сетки

  g_pMesh = new CMesh(); // Создать нашу сетку

  if( !g_pMesh->LoadHeightmap( "terrain.bmp", // Загрузка карты высот

        MESH_HEIGHTSCALE, MESH_RESOLUTION ) )

  {

    MessageBox( NULL, "Error Loading Heightmap", "Error", MB_OK );

    return false;

  }

 

  // Проверка поддержки буферов

#ifndef NO_VBOS

  g_fVBOSupported = IsExtensionSupported( "GL_ARB_vertex_buffer_object" );

  if( g_fVBOSupported )

  {

    // Указатели на функции GL

    glGenBuffersARB = (PFNGLGENBUFFERSARBPROC) wglGetProcAddress("glGenBuffersARB");

    glBindBufferARB = (PFNGLBINDBUFFERARBPROC) wglGetProcAddress("glBindBufferARB");

    glBufferDataARB = (PFNGLBUFFERDATAARBPROC) wglGetProcAddress("glBufferDataARB");

    glDeleteBuffersARB = (PFNGLDELETEBUFFERSARBPROC) wglGetProcAddress("glDeleteBuffersARB");

    // Загрузить наши данные в память графической карты

    g_pMesh->BuildVBOs(); // Построить буфер

  }

#else /* NO_VBOS */

  g_fVBOSupported = false;

#endif

 

Функцию IsExtensionSupported можно найти на OpenGL.org. Здесь моя вариация ее.

 

bool IsExtensionSupported( char* szTargetExtension )

{

  const unsigned char *pszExtensions = NULL;

  const unsigned char *pszStart;

  unsigned char *pszWhere, *pszTerminator;

 

  // Имя расширения не должно содержать пробелы

  pszWhere = (unsigned char *) strchr( szTargetExtension, ' ' );

  if( pszWhere || *szTargetExtension == '\0' )

    return false;

 

  // Получить строку расширения

  pszExtensions = glGetString( GL_EXTENSIONS );

 

  // Поиск в строке расширений

  pszStart = pszExtensions;

  for(;;)

  {

    pszWhere = (unsigned char *) strstr( (const char *) pszStart, szTargetExtension );

    if( !pszWhere )

      break;

    pszTerminator = pszWhere + strlen( szTargetExtension );

    if( pszWhere == pszStart || *( pszWhere - 1 ) == ' ' )

      if( *pszTerminator == ' ' || *pszTerminator == '\0' )

        return true;

    pszStart = pszTerminator;

  }

  return false;

}

 

Это довольно просто. Некоторые просто используют поиск подстроки в строке через strstr, но в данном случае расширение не считается, найдено, если найдено просто совпадение в строке расширений. И я не собираюсь с этим спорить.

 

Мы почти закончили! Теперь все что надо сделать, так это послать данные в GPU.

 

void Draw (void)

{

  glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // Очистить экран и буфер глубины

  glLoadIdentity (); // Сброс матрицы модели и вида

 

  // Взять FPS

  if( GetTickCount() - g_dwLastFPS >= 1000 ) // Когда прошла секунда...

  {

    g_dwLastFPS = GetTickCount(); // Обновить переменную времени

    g_nFPS = g_nFrames; // Сохранить FPS

    g_nFrames = 0; // Сброс счетчика FPS

 

    char szTitle[256]={0}; // Построить строку заголовка

    sprintf( szTitle, "Lesson 45: NeHe & Paul Frazee's VBO Tut - %d Triangles, %d FPS", g_pMesh->m_nVertexCount / 3, g_nFPS );

    if( g_fVBOSupported ) // Включим упоминание о буфере

      strcat( szTitle, ", Using VBOs" );

    else

      strcat( szTitle, ", Not Using VBOs" );

    SetWindowText( g_window->hWnd, szTitle ); // Задать заголовок

  }

  g_nFrames++; // Увеличить счетчик FPS

 

  // Сдвинуть камеру

  glTranslatef( 0.0f, -220.0f, 0.0f ); // Сдвинуть вдоль ландшафта

  glRotatef( 10.0f, 1.0f, 0.0f, 0.0f ); // Смотреть слегка вниз

  glRotatef( g_flYRot, 0.0f, 1.0f, 0.0f ); // Вращение камеры

 

Все довольно просто. Надо каждую секунду, рассчитать FPS из счетчика кадров и сбросить счетчик кадров. Затем мы сдвигаем камеру над ландшафтом (если вы измените его, то надо будет настроить это место), и делаем несколько вращений. g_flYRot увеличивается в функции Update.

 

Для использования вершинных массивов (и буферов), надо послать в OpenGL данные, которые заданы в обычной памяти. Поэтому на первом шаге надо разрешить на клиенте состояние GL_VERTEX_ARRAY и GL_TEXTURE_COORD_ARRAY. Затем мы задаем наши указатели. Я сомневаюсь, что это надо делать каждый кадр, если у нас только одна сетка, но мы будем задавать это каждый раз, чтобы избежать заумного кода.

 

Для каждого типа данных мы используем соответствующую функцию - glVertexPointer и glTexCoordPointer. Использовать их очень просто – передать нужное число переменных для задания точки (три переменных для задания положения, два для задания текстурных координат), данные передаются с плавающей запятой, шаг между данными, и указатель на данные. Можно использовать glInterleavedArrays и сохранить все данные в один большой буфер, я сохраняю их раздельно, для того чтобы показать, как использовать множественные буфера.

 

Использование буферов немного отличается. Надо всего лишь не указывать указатель на данные, а передать ноль, вот смотрите:

 

  // Задать указатели на наши данные

  if( g_fVBOSupported )

  {

    glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOVertices );

    // Задать указатель на вершины в вершинном буфере

    glVertexPointer( 3, GL_FLOAT, 0, (char *) NULL );

    glBindBufferARB( GL_ARRAY_BUFFER_ARB, g_pMesh->m_nVBOTexCoords );

    // Задать указатель на TexCoord в буфере TexCoord

    glTexCoordPointer( 2, GL_FLOAT, 0, (char *) NULL );

  } else

  {

    // Задать указатель на вершины в вершинном буфере

    glVertexPointer( 3, GL_FLOAT, 0, g_pMesh->m_pVertices );

    // Задать указатель на TexCoord в буфере TexCoord

    glTexCoordPointer( 2, GL_FLOAT, 0, g_pMesh->m_pTexCoords );

  }

 

И знаете что? Визуализация проще.

 

  // Визуализация

  // Нарисовать все треугольники одним вызовом

  glDrawArrays( GL_TRIANGLES, 0, g_pMesh->m_nVertexCount );

 

Здесь мы используем glDrawArrays для того чтобы послать наши данные OpenGL. glDrawArrays проверяет какое состояние клиента доступно, и затем использует их указатели для визуализации. Мы говорим, какой тип геометрических данных, индекс, откуда стартовать, и как много вершин для визуализации. Есть много других путей, которыми вы можете послать данные на отображение, таких как glArrayElement, но это метод быстрее всех. Как вы видите glDrawArrays, не использует функции glBegin / glEnd. В них нет необходимости.

 

glDrawArrays я выбрал еще потому что вершины не используются совместно разными треугольниками – это невозможно. Насколько я знаю лучший способ оптимизировать память – это использовать полоски треугольников, но это выходит за рамки этого урока. Так же вы должны знать, что нормали эксплуатируются «один в один» с вершинами, это означает, что если вы используете нормали, каждая вершина должна иметь соответствующую нормаль. Учтите, что вычисление по-вершинных нормалей, будет повышать визуальное качество.

 

Теперь осталось запретить массивы вершин, и мы заканчиваем.

 

  // Запрет указателей

  glDisableClientState( GL_VERTEX_ARRAY ); // запрет вершинных массивов

  // Запрет массива текстурных координат

  glDisableClientState( GL_TEXTURE_COORD_ARRAY );

}

 

Если Вы хотите больше информации об объектах вершинных буферов, я рекомендую прочитать информацию о них в реестре расширений SGI - http://oss.sgi.com/projects/ogl-sample/registry. Это более утомительно, чем прочитать урок, но даст вам значительно больше информации.

 

Вот урок завершен. Если вы нашли ошибки или у вас есть замечания, или просто вопросы, вы можете написать мне paulfrazee@cox.net

 

© Paul Frazee

PMG  20 декабря 2006 (c)  Сергей Анисимов