VBO другими словами

Сначала нужно создать имена буферным объектам:

VBOid = new int[2];
GL.GenBuffers(2, VBOid);

Существует два буферных объекта: для хранения массива вершин (ArrayBuffer) и для хранения массива индексов (ElementArrayBuffer).

Для этих буферов мы создали два имени.

Теперь связываем имена с буферными объектами:

GL.BindBuffer(BufferTarget.ArrayBuffer, VBOid[0]); //связываем VBO
GL.BindBuffer(BufferTarget.ElementArrayBuffer, VBOid[1]); //связываем IBO

Нам необходимо создать в нужном формате два массива для хранения информации о вершинах и индексах, которые мы передадим в только что созданные буферные объекты. Какой именно будет формат, решать Вам. Возможно, для быстрой загрузки Вы захотите хранить объекты в файлах в этом формате.

В массиве для вершин минимально требуется только координаты вершин (x, y, z). Но, если требуются нормали и текстурные координаты, то обычно можно увидеть данные в таком виде:

vertex array format

sizeof(float) * 3 для указания позиции вершины, sizeof(float) * 3 на вектор нормали и sizeof(float) * 2 на текстурные координаты. Если на машине размер типа float равен 4 байтам, то на описание точки требуется 4*3 + 4*3 + 4*2 = 4*8 =32 байта. Всего размер массива будет (sizeof(float) * 8) * vertex_count.

Массив индексов состоит из целочисленных данных типа int. Каждый элемент индекса указывает на блок данных из массива вершин. Т.к. массив вершин одномерный, то позиция блока с нужными данными находится по адресу index * sizeof(float) * 8.

Передаём буферу вершин указатель на массив вершин, в котором находятся данные в нашем формате (массив с именем, например, "vertexes"):

GL.BufferData(BufferTarget.ArrayBuffer, (IntPtr)(vertexes.Length * sizeof(float)), vertexes, BufferUsageHint.StaticDraw); //записываем в память VBO

Данные из массива вершин перемещаются в т.н. "хранилище данных". Параметр-подсказка BufferUsageHint со значением StaticDraw "подсказывает" машине, что данные будут меняться редко.

Точно также передаём буферу индексов указатель на массив индексов (с именем, например, "indexes"):

GL.BufferData(BufferTarget.ElementArrayBuffer, (IntPtr)(indexes.Length * sizeof(int)), indexes, BufferUsageHint.StaticDraw); //записываем в память IBO

Данные переданы в буфер. Обходя буфер индексов, OpenGL будет искать данные в буфере вершин, и теперь нужно указать, по какому адресу искать данные о позициях точек, нормалей и текстурных координат:

GL.VertexPointer(3, VertexPointerType.Float, sizeof(float) * 8, 0); //координаты точек идут первыми, смещение 0
GL.NormalPointer(NormalPointerType.Float, sizeof(float) * 8, sizeof(float) * 3); //вектор нормали идет после данных о позиции
GL.TexCoordPointer(2, TexCoordPointerType.Float, sizeof(float) * 8, sizeof(float) * 6); //текстурные координаты после вектора нормали

Эти функции указывают расстояние между данными и начальную позицию данных в буфере вершин. Расстояние одинаковое sizeof(float) * 8 (длина блока данных о вершине), а начальная позиция зависит от положения внутри блока.

Осталось указать OpenGL, что мы в данный момент хотим вывести на экран всю информацию о точках:

GL.EnableClientState(ArrayCap.VertexArray);
GL.EnableClientState(ArrayCap.NormalArray);
GL.EnableClientState(ArrayCap.TextureCoordArray);

Эти команды можно давать в каждом кадре.

И в каждом кадре даем команду рисования:

GL.DrawElements(BeginMode.Triangles, indexes.Length, DrawElementsType.UnsignedInt, IntPtr.Zero);

Эта функция находит буфер индексов, берет каждые 3 индекса для рисования треугольников (т.к. мы указали BeginMode.Triangles), находит по ним 3 блока данных из буфера вершин, по данным о позициях (из функций VertexPointer, NormalPointer и TexCoordPointer) берет нужные данные из буфера вершин, рисует.