3.a Vertex Buffer Objects
Введение
Достоинство VBO заключается в том, что мы можем заставить OpenGL сохранять информацию используемую для рисования (координаты, цвет, текстуры и нормали) непосредственно в памяти видеокарты, вместо того чтобы хранить их в системной памяти и передавать напрямую в видеокарту, когда нам это нужно. Конечно, это можно сделать и с помощью дисплейных списков (display list), но по сравнению с ними применяя VBO можно получить указатель на данные в видеопамяти и в случае необходимости изменять эти данные. Такой подход приводит к действительно огромному повышению производительности для динамических массивов графических примитивов и на протяжении многих лет является лучшим решением для хранения статических и динамических массивов (meshes).
Создаём буфер
Работа с буферными объектами похожа на работу с текстурами. Сначала создаем/удаляем указатель на объект, связываем его с существующими данными или заполняем новыми данными. В этом уроке нам понадобиться два объекта. Первый это VBO (Вершинный Буфер) содержащий информацию о вершинах (в нашем случае это координаты и цвет) и IBO (Индексный Буфер) ссылающийся на данные из VBO для индексации вершин треугольников. После создания VBO и IBO мы можем отрисовать всю фигуру с помощью одного вызова GL.DrawElements().
Сначала создаем два объекта:
uint[] VBOid = new uint[ 2 ]; GL.GenBuffers( 2, out VBOid );
Маловероятно, но возможно что OpenGL не хватит памяти для загрузки данных или то, что требуемое расширение не поддерживается. Это можно проверить вызовом GL.GetError().
Удаляем буфер
Драйвер OpenGL удаляет наши массивы вершин и прочую информацию при удалении контекста воспроизведения. Несмотря на это, удалить самостоятельно данные после использования это хорошая идея. Это можно следующим путем:
GL.DeleteBuffers( 2, ref VBOid );
Назначение буфера
Для того, чтобы OpenGL понять какую роль выполняет созданный буфер, необходимо пометить его как BufferTarget.ArrayBuffer или BufferTarget.ElementArrayBuffer. Первый тип используется для хранения координат, uv-координат, нормалей и так далее (VBO). Второй тип хранит индексы вершин (IBO).
GL.BindBuffer( BufferTarget.ArrayBuffer, VBOid[ 0 ] ); GL.BindBuffer( BufferTarget.ElementArrayBuffer, VBOid[ 1 ] );
Не обязательно всегда создавать вершинный и индексный буфер. Для примера, вы можете сохранить вершины в VBO буфере, а индексы держать в системной памяти. Также, необязательно связывать вершинный и индексный буфер вместеe, например, для одного вершинного буфера можно иметь два разных индексных буфера.
Есть две вещи о которых нельзя забывать:
1) Перед работой с VBO, требуется активировать GL.EnableClientState(EnableCap.VertexArray). Если используются нормали - GL.EnableClientState(EnableCap.NormalArray), это похоже на работу с классическими массивами вершин.
2) Все команды связанные с массивами вершин будут применяться к объектным буферам. Для отключения VBO, назначьте вершинному и индексному буферу значение ноль
GL.BindBuffer( BufferTarget.ArrayBuffer, 0 ); GL.BindBuffer( BufferTarget.ElementArrayBuffer, 0 );
Заполнение данными
Есть несколько путей, чтобы заполнить объектный буфер данными. Здесь мы покажем пример использования GL.BufferData вместе с прямым доступ к видеопамяти. После того как вы достаточно хорошо освоитесь с GL.BufferData можно использовать и GL.BufferSubData.
- GL.BufferData
We will start by preparing the IBO, it would not make a difference if we set up the VBO first, we simply start with the shorter one.We make sure the correct object is bound (it is not required to do this, if the buffer is already bound. Just here to clarify on which object we currently work on)
GL.BindBuffer( BufferTarget.ElementArrayBuffer, VBOid[ 1 ] );
In the example application ushort has been used for Indices, because 16 Bits [0..65535] are more available Vertices than used by most real-time rendered meshes, however the mesh could index way more Vertices using a type like uint. Using
ushort, OpenGL will store this data as 2 Bytes per index, saving memory compared to a 4 Bytes UInt32 per index.The function GL.BufferData's first parameter is the target we want to use, the second is the amount of memory (in bytes) we need allocated to hold all our data. The third parameter is pointing at the data we wish to send to the graphics card, this can be IntPtr.Zero and you may send the data at a later stage with GL.MapBuffer (more about this later). The last parameter is an optimization hint for the driver, it will place your data in the best suited place for your purposes.
GL.BufferData( BufferTarget.ElementArrayBuffer, (IntPtr) ( Indices.Length * sizeof( ushort ) ), Indices, BufferUsageHint.StaticDraw );
That's all, OpenGL now has a copy of Indices available and we could dispose the array, assuming we have the Index Count of the array stored in a variable for the draw call later on.
Now that we've stored the indices in an IBO, the Vertices are next. Again, we make sure the binding is correct, give a pointer to the Vertex count, and finally the usage hint.
GL.BindBuffer( BufferTarget.ArrayBuffer, VBOid[ 0 ] ); GL.BufferData( BufferTarget.ArrayBuffer, (IntPtr) ( Vertices.Length * 8 * sizeof( float ) ), Vertices, BufferUsageHint.StaticDraw );
There's a table at the bottom of this page, explaining the options in the enum BufferUsageHint in more detail.
- GL.MapBuffer / GL.UnmapBuffer
While the first described technique to pass data into the objects required a copy of the data in system memory, this alternative will give us a pointer to the video memory reserved by the object. This is useful for dynamic models that have no copy in client memory that could be used by GL.BufferData, since you wish to rebuild it every single frame (e.g. fully procedural objects, particle system).
First we make sure that we got the desired object bound and reserve memory, the pointer towards the Indices is actually IntPtr.Zero, because we only need an empty buffer.
GL.BindBuffer( BufferTarget.ElementArrayBuffer, VBOid[ 0 ] ); GL.BufferData( BufferTarget.ElementArrayBuffer, (IntPtr) ( Indices.Length * sizeof( ushort ) ), IntPtr.Zero, BufferUsageHint.StaticDraw );
Note that you should change BufferUsageHint.StaticDraw properly according to what you intend to do with the Data, there's a table at the bottom of this page. Now we're able to request a pointer to the video memory.
IntPtr VideoMemoryIntPtr = GL.MapBuffer(BufferTarget.ElementArrayBuffer, BufferAccess.WriteOnly);
Valid access flags for the pointer are BufferAccess.ReadOnly, BufferAccess.WriteOnly or BufferAccess.ReadWrite, which help the driver understand what you're going to do with the data. Note that the data's object is locked until we unmap it, so we want to keep the timespan over which we use the pointer as short as possible. We may now write some data into the buffer, once we're done we must release the lock.
unsafe { fixed ( ushort* SystemMemory = &Indices[0] ) { ushort* VideoMemory = (ushort*) VideoMemoryIntPtr.ToPointer(); for ( int i = 0; i < Indices.Length; i++ ) VideoMemory[ i ] = SystemMemory[ i ]; // simulate what GL.BufferData would do } } GL.UnmapBuffer( BufferTarget.ElementArrayBuffer );
The pointer is now invalid and may not be stored for future use, if we wish to modify the object again, we have to call GL.MapBuffer again.
Further reading
Visit this link in order to tell OpenGL about the composition of your Vertex data, and this link for drawing the data.
Optimization:
One hint from the nVidia whitepaper was regarding the situation, if we want to update all data in the buffer object by using GL.MapBuffer and not retrieve any of the old data. Although this is a bad idea, because mapping the buffer is a more expensive operation than just calling GL.BufferData, it might be necessary in cases where you have no copy of the data in system memory, but build it on the fly. The solution to making this somewhat efficient is first calling GL.BufferData with a IntPtr.Zero again, which tells the driver that the old data isn't valid anymore. Calling GL.MapBuffer will return a new pointer to a valid memory location of the requested size to write to, while the old data will be cleaned up once it's not used in any draw operations anymore.
Also note that either reading from a VBO or wrapping it into a Display List is very slow and should both be avoided.
Table 1:
BufferUsageHint.Static... Assumed to be a 1-to-n update-to-draw. Means the data is specified once (during initialization).
BufferUsageHint.Dynamic... Assumed to be a n-to-n update-to-draw. Means the data is drawn multiple times before it changes.
BufferUsageHint.Stream... Assumed to be a 1-to-1 update-to-draw. Means the data is very volatile and will change every frame.
...Draw Means the buffer will be used to sending data to GPU. video memory (Static|StreamDraw) or AGP (DynamicDraw)
...Read Means the data must be easy to access, will most likely be system or AGP memory.
...Copy Means we are about to do some ..Read and ..Draw operations.
...
Начнем с подготовки IBO, здесь нет разницы с чего начинать первыми. Для простоты сделаем то, что быстрее всего.
Мы должны удостоверитьтся в том что правильно связали объекты (этого не требуется если объекты уже были связаны. Здесь мы просто вносим ясность с каким объектом ведется работа)
GL.BindBuffer( Version15.ElementArrayBuffer, VBOid[ 1 ] );
В этом примере используется тип ushort для граней, потому максимальное значение для этого типа 65535 больше чем используемое нами Сторон. При использовании настоящих наборов это число может быть и больше и поэтому стоит задумать об использовании типа наподобие uint. В данном случае используя ushort мы экономим два байта на каждый индекс как если бы мы использовали тип UInt32.
У функции GL.BufferData первый параметр это назначение по которому мы хотим использовать, второй это количество памяти (в байтах) которое мы хотим использовать для хранения своих данных. Третий параметр это указатель на данные которые мы хотим отправить видеокарте, его тип должен быть IntPtr (целочисленный указатель). Здесь можно использовать IntPtr.Zero указатель на ноль если мы заполним данные на последующем этапе с помощью GL.MapBuffer (подробнее об этом позже). Последний параметр это подсказка драйверу видеокарте об оптимизации, это поможет расположить твои данные в наилучшем месте по твоему желанию. В таблице 1 приведен полный список опций.
GL.BufferData( Version15.ElementArrayBuffer, (IntPtr) ( Indices.Length * sizeof( ushort ) ), Indices, Version15.StaticDraw );
На этом всё, OpenGL скопирует доступные грани и мы можем освободить массив на который ссылаясь. Теперь у нас есть Массив Индексов который можно использовать позже.
Итак, мы сохранили индисы в IBO, теперь пора считать Вершины. Еще раз убедимся что связали всё правильно и окончим оптимизацией.
GL.BindBuffer( Version15.ArrayBuffer, VBOid[ 0 ] );
GL.BufferData( Version15.ArrayBuffer, (IntPtr) ( Vertices.Length * 6 * sizeof( float ) ), Vertices, Version15.StaticDraw );
Итак мы сохранили всю информацию о графике и перед вызовом GL.DrawElements должны кое что сделать. Вершины которые мы хотим использовать содержат не только координаты, но и цвета с которыми будут отображаться. GL.InterleavedArrays позволяет включить/выключить необходимое состояние клиента для правильной интерпретации наших загруженных данных OpenGL , первый параметр говорит о том что у нас есть 3 float для описания цвета (C3F) и 3 float для описания позиции (V3F). Второй параметр это шаг с которым должен быть совершен переход от первой вершины ко второй, третьей и так далее. Последний параметр должен быть указатель на Indeces, но мы уже загрузили его в видеопамять, поэтому второй раз указывать не нужно.
GL.InterleavedArrays( InterleavedArrayFormat.C3fV3f, 0, null );
Преимущество этой команды в том что она позволяет ясно объяснить OpenGL с какие данные нужно выводить и также это позволяет оптимизировать память. Запомни что GL.InterleavedArrays сработает только если включены EnableCap.VertexArray, EnableCap.ColorArray или изменялись GL.VertexPointer или GL.ColorPointer. Незабудь включить их обратно иначе ничего не произойдет.
Для более детального описания о состояниях Массива Вершин обращайся к RedBook или к описанию команд OpenGL.
- Printer-friendly version
- Login or register to post comments


Comments
Re: Vertex Buffer Objects
Этот перевод ещё более непонятный, чем источник. Цель какая преследовалась, запутать ещё больше?
Re: Vertex Buffer Objects
Цель была такая - пропустить через подстрочник для того, чтобы вернутся к правке позже.