вторник, 21 февраля 2012 г.

Основы VBO в OpenGL. Часть первая, первый квадрат


  1. Часть первая, первый квадрат
  2. Часть вторая, добавляем атрибуты
  3. Часть третья, индексный буфер
  4. Часть четвертая, динамика
  5. Часть пятая, дополнительные возможности
  6. Часть шестая, VAO
  7. Ссылки по теме
Я уже многократно упоминал о VBO, которое даже входит в название моего движка, потому я решил здесь опубликовать свою старую ознакомительную статью об этом самом VBO.

VBO (Vertex Buffer Objects) – технология, позволяющая хранить координаты вершин совместно с их атрибутами в видеопамяти.
В отличии от использования блока glBegin/glEnd, при котором на каждом кадре вся геометрия передается GPU по очень медленной шине PCIEx, при использовании VBO все геометрия загружается в видеопамять только один раз, на этапе инициализации, после чего мы просто ссылаемся на эти данные. Это во-первых позволят существенно разгрузить шину для более важных задач, во-вторых - это приводит к существенному повышению производительности, так как GPU может незамедлительно приступать к рендерингу, не дожидаясь пока будут получены данные от CPU. Ну и пропускная способность видеопамяти в десятки раз выше пропускной способности PCIEx (4Гб/сек против 148Гб/сек), благодаря чему все операции копирования в видеопамяти происходят существенно быстрее чем при загрузке данных со стороны CPU. Используя приведенные выше особенности можно существенно повысить как производительность так и гибкость рендера, но обо всем по-порядку.


Работа с VBO сводится к нескольким этапам –
  1. создание буферов, передача в буферы координат и атрибутов вершин, передача данных в видеопамять
  2. активация нужного буфера, рендеринг буфера и деактивация.
  3. удаления данных в видеопамяти
Рассмотрим поэтапно данный процесс. Этап первый – создание буфера VBO
Листинг 1:
1
2
3
4
5
6
7
var vId: Cardinal;
begin
  glGenBuffers( 1, @vId );
  glBindBuffer(GL_ARRAY_BUFFER, vId );
  glBufferData(GL_ARRAY_BUFFER, sizeof(GLFLoat)*3*Count, @VertexBuffer[0], GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER,0);
end;
Разберем этот код – в первую очередь для работы с VBO нам нужно выделить переменную в которой будет храниться идентификатор нашего буферного объекта, в качестве такой переменной у нас используется vId: Cardinal, как вариант можно использовать тип данных OpenGL GLUInt, что по сути одно и то же. Далее нам нужно попросить OpenGL выдать нам свободный идентификатор, делается это в строке (3) командой glGenBuffers( 1, @vId ). Первый параметр указывает на количество буферов которые мы хотим сгенерировать, в нашем случае нужен лишь один буфер, о чем мы и сообщили.
Теперь у нас есть идентификатор на пока что пустой буфер объекта, следовательно самое время его чем-то заполнить, но прежде чем заполнять – нужно сделать данный буфер активным, делается это командой (4) glBindBuffer(GL_ARRAY_BUFFER, vId ). В качестве последнего параметра выступает наш идентификатор, а в качестве первого – специальная константа, говорящая OpenGL что данный буфер будет использоваться для хранения вершинных атрибутов.

К вершинным атрибутам в первую очередь относятся координаты вершин, нормали, текстурные координаты и цвет вершины. Существует еще несколько типов буферов, но они практически не используются и многие уже убраны из OpenGL3.x, потому мы их рассматривать не будем. Кроме стандартных вершинных атрибутов существуют так же пользовательские вершинные атрибуты. но их мы рассмотрим позже.

Теперь нам необходимо передать эти самые вершинные атрибуты из оперативной памяти в видеопамять, для этого служит  (5)  команда  glBufferData. В качестве первого параметра передается все та же константа, говорящая OpenGL что мы все еще работаем с буфером вершин, в качестве второго параметра мы задаем размер данного буфера в байтах. В Вершинном буфере хранятся координаты каждой вершины. Для удобства мы создадим отдельную структуру для хранения координат вершин - TVertex:
TVertex = record
   X,Y,Z: single;
End;
Таким образом, одна вершина будет у нас занимать в памяти 3 координаты*4байта на каждую=12 байт. Соответственно, если у нас 100 вершин, то это число еще нужно умножить на 100, или в общем виде – на Count. Сам же буфер у нас будет объявлен как обычный одномерный массив типа TVertex:
Var VertexBuffer: Array of TVertex;
…..
Setlength(VertexBuffer, Count);
Указатель на данный массив и выступает у нас в качестве третьего параметра функции glBufferData.
Ну и четвертый, последний параметр, указывает GPU как часто содержимое данного буфера будет обновляться. Существует несколько основных типов:
GL_STATIC_DRAW            – данные будут очень редко обновляться
GL_DYNAMIC_DRAW       данные будут обновляться, но не каждый кадр
GL_STREAM_DRAW          – данные будут обновляться каждый кадр.
Это позволяет компилятору более эффективно работать с данными буферами.

Теперь наши данные находятся в видеопамяти, и с чистой совестью мы можем удалять данные из оперативной памяти (если в них больше нет надобности). Так же можно освободить активный буфер, так как до тех пор, пока буфер VBO активен, все операции рисования будут пытаться работать с VBO. Активация следующего буфера автоматически отключает предыдущий. Для полной деактивации буфера VBO необходимо указать в качестве идентификатора буфера "0" (строка 6):
glBindBuffer(GL_ARRAY_BUFFER,0);

Все, на этом первый этап – генерации вершинного буфера – завершен. Данная процедура выполняется только один раз при создании буфера, в дальнейшем нам нужно будет лишь указывать его в качестве источника данных для рисования, собственно переходим ко второму этапу – активации и отрисовки заданного буфера:
Листинг 2:
1
2
3
4
5
6
7
begin
  glEnableClientState( GL_VERTEX_ARRAY );
  glBindBuffer( GL_ARRAY_BUFFER, vId );
  glVertexPointer( 3, GL_FLOAT, 0, nil );
  glDrawArrays(GL_TRIANGLES , 0, VertexCount);
  glDisableClientState(GL_VERTEX_ARRAY);
end;
Рассмотрим данный код – первое что нам нужно сделать – переключить состояние OpenGL на использование вершинных буферов (иначе будут использована более старая и более медленная технология VBA), это делается вызовом команды glEnableClientState(GL_VERTEX_ARRAY). В качестве параметра этой команды выступает тип атрибута. Вышеперечисленным типам атрибутов соответствуют такие константы:
Вершинные координаты       -           GL_VERTEX_ARRAY
Нормали                                  -           GL_NORMAL_ARRAY
Текстурные координаты        -           GL_TEXTURE_COORD_ARRAY
Цвет вершины                         -           GL_COLOR_ARRAY

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

После того, как нужный буфер сделан активным мы должны указать GPU что из этого буфера у нас будут браться координаты вершин (в одном буфере у нас могут содержаться несколько различных атрибутов), для этого у нас служит команда glVertexPointer( 3, GL_FLOAT, 0, nil ).
Первый параметр данной команды говорит GPU о том, сколько координат мы хотим передать, в нашем случае у каждой вершины у нас 3 координаты – X,Y,Z, потому мы и указываем первым параметром число 3. Второй параметр говорит GPU в каком виде у нас содержатся эти данные, вспоминаем нашу структуру TVertex, объявленную выше – там у нас данные имеют тип single, который эквивалентен типу GL_FLOAT, потому мы и указываем в качестве второго параметра тип GL_FLOAT. Третий параметр у нас задает расстояние между однотипными данными. Как я уже упоминал выше – в одном буфере у нас могут содержаться несколько вершинных атрибутов, все они расположены друг за другом, вот мы и должны указать сколько блоков памяти размером GL_FLOAT мы должны пропустить, чтоб добраться до интересующих нас данных. В нашем случае у нас всего 1 атрибут, потому мы указываем в качестве параметра 0, что говорит что координаты следуют друг за другом. Последний параметр так же необходим при помещении нескольких атрибутов в один буфер, и задает начальное смещение этих данных в буфере. В случае одного атрибута это смещение задается как nil

Более подробно о упаковке нескольких атрибутов в один буфер можно почитать в справке по команде glInterleavedArrays. Данная технология несколько устарела, потому я это пропущу. 

Итак, после того как мы сделали активным наш вершинный буфер и указали GPU где искать наши координаты вершин – самое время отрисовать их, для этого используется команда:
glDrawArrays(GL_TRIANGLES , 0, VertexCount);
Данная команда говорит GPU что нужно отрисовать VertexCount вершин, при этом 3 идущие друг за другом вершины формируют один треугольник, о чем говорит параметр GL_TRIANGLES. Вместо GL_TRIANGLES вы можете использовать любой стандартный тип полигона, к примеру GL_TRIANGLE_STRIP, GL_QUADS и прочие. Второй параметр позволяет начать рисование не с первого элемента массива, в нашем случае 0 означает что будут отрисованы все треугольники, начиная с самого первого.
После того как все треугольники нарисованы, необходимо восстановить состояние OpenGL, это делается функцией glDisableClientState(GL_VERTEX_ARRAY), в качестве которого выступает тип активного буфера.
Данный этап выполняется каждый раз, когда нужно отрендерить нашу геометрию.

Использование технологию VAO, можно свести весь этап активации буферов VBO к одной команде, но это мы рассмотрим позже.

Ну и третий этап - после того, как программа отработала, или после того, как какой-то из буферов стал нам не нужен нам нужно освободить занимаемую им видеопамять, делается это очень легко:
glBindBuffer( GL_ARRAY_BUFFER, 0 );          
glDeleteBuffers(1,@vId);
Тоесть мы вначале делаем текущий буфер неактивным, после чего командой glDeleteBuffers удаляем буфер с идентификатором vId.

Вот собственно и все что нужно чтоб вывести любую геометрию, теперь давайте попробуем объединить все написанное выше в работающий пример, для этого оформим приведенный выше код в виде процедур, первый этап назовем инициализацией (VBOInit), второй – рисованием (VBODraw) и третий – освобождением ресурсов (VBOFree).
Так же нам потребуется объявить некоторые глобальные переменные, в частности переменную vId (Vertex Id), массив координат вершин «Var VertexBuffer: Array of TVertex» и объявить новый тип данных «TVertex = record  X,Y,Z: single; End

Запишем все это:
Листинг 3:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Type
  TVertex = record 
     X,Y,Z: single;
  end;
var
   vId:integer;
   VertexBuffer: Array of TVertex;
   Ready:Boolean=false;
procedure VBOInit;
var Count:integer;
begin
  Count:=high(VertexBuffer)+1;
  glGenBuffers( 1, @vId );
  glBindBuffer(GL_ARRAY_BUFFER, vId );
  glBufferData(GL_ARRAY_BUFFER, sizeof(GLFLoat)*3*Count, @VertexBuffer[0], GL_STATIC_DRAW);
  glBindBuffer(GL_ARRAY_BUFFER,0);
end;

procedure VBODraw;
var Count:integer;
begin
  Count:=high(VertexBuffer)+1;
  glEnableClientState( GL_VERTEX_ARRAY );
  glBindBuffer( GL_ARRAY_BUFFER, vId );
  glVertexPointer( 3, GL_FLOAT, 0, nil );
  glDrawArrays(GL_TRIANGLES , 0, Count);
  glDisableClientState(GL_VERTEX_ARRAY);
end;

procedure VBOFree;
begin
  glBindBuffer( GL_ARRAY_BUFFER, 0 );      
  glDeleteBuffers(1,@vId);
end; 
Теперь осталось только заполнить буфер вершин нашими данными и отправить его на рисование. Я поместил инициализацию буфера в конструктор формы:
Листинг 4:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
procedure TForm1.FormCreate(Sender: TObject);
begin
   Setlength(VertexBuffer,6);//Создаем буффер под 6 вершин
   //Создаем квадрат, состоящий из двух треугольников
   VertexBuffer[0].X:=-1; VertexBuffer[0].Y:=1; VertexBuffer[0].Z:=0;
   VertexBuffer[1].X:=-1; VertexBuffer[1].Y:=-1;VertexBuffer[1].Z:=0;
   VertexBuffer[2].X:= 1; VertexBuffer[2].Y:=1; VertexBuffer[2].Z:=0;
   VertexBuffer[3].X:= 1; VertexBuffer[3].Y:=1; VertexBuffer[3].Z:=0;
   VertexBuffer[4].X:=-1; VertexBuffer[4].Y:=-1;VertexBuffer[4].Z:=0;
   VertexBuffer[5].X:= 1; VertexBuffer[5].Y:=-1;VertexBuffer[5].Z:=0;
   //Инициализируем наш буфер

   VBOInit;
   Ready:=true; //ставим флаг что буфер готов к рендерингу
end;
Данный код просто создает массив и заполняет его данными о 4-х вершинах, из которых составляется 2 треугольника (три последовательные вершины задают треугольник), после чего происходит инициализация нашего буфера VBO.

Сразу напишем код, удаляющий наш буфер после завершения работы, для этого в событии onClose пишем такой вот код:
Листинг 5:
1
2
3
4
5
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Ready:=false;
  VBOFree;
end;

Для демок я использовал GLScene, чтоб заставить GLScene выполнить код OpenGL существует специальный компонент - GLDirectOpenGL, у которого есть событие onRender, вот туда мы и помещаем вызов нашей команды рисования. Важное замечание - в последних версиях GLScene изменился способ прямой работы с OpenGL, но дабы не привязываться к синтаксису одного движка я продолжу приводить код на чистом OpenGL.
Листинг 6:
1
2
3
4
5
procedure TForm1.GLDirectOpenGL1Render(Sender: TObject;
  var rci: TRenderContextInfo);
begin
  if Ready then VBODraw;
end;
Здесь мы просто проверяем готов ли наш буфер и если готов, то вызываем процедуру рисования.

При использовании GLScene требуется принудительная перерисовка вьюпорта, для этого, событии onProgress каденсера, нужно вызвать метод GLSceneViewer1.Invalidate:
Листинг 7:
1
2
3
4
5
procedure TForm1.GLCadencer1Progress(Sender: TObject; const deltaTime,
  newTime: Double);
begin
   GLSceneViewer1.Invalidate;
end;
Теперь можно компилировать и смотреть на результат:
Рис.1. Первый квадрат.
Вот мы и нарисовали наш первый примитив с использованием технологии VBO. Пока что он у нас серый, без нормалей, текстур и прочих атрибутов, именно ими мы и займемся в следующем уроке.

Бинарник с исходными кодами примера: Demo1

32 комментария:

  1. Этот урок был первым моим знакомством с VBO. Все четко и понятно. Спасибо.

    ОтветитьУдалить
  2. Спасибо но жаль что уроки не на си.Если так нужна производительность,то почему бы не перейти на C/C++ ?

    ОтветитьУдалить
    Ответы
    1. dzanis, это уроки по OpenGL а не по языкам программирования. Кроме функций и массивов никакие другие средства языка не используются, потому без разницы на чем оно написано и тем более это не никак не влияет на производительность, разве что копипаст усложняется :)

      К слову, то что на C/C++ быстрее - это заблуждение годов 90-х. Во-первых - сравнивается не производительность языка программирования а эффективность генерируемого компилятором кода и далеко не все компиляторы выдают одинаково эффективный код. Во-вторых - разница между наиболее эффективными компиляторами составляет порядка 1-2%, причем на разных задачах в пользу разных компиляторов, потому не стоит зацикливаться на языке ради мифической производительности.
      Вообщем рекомендую глянуть хотя бы это сравнение (C#, Java, C, C++, Delphi):
      http://webandlife.blogspot.com/2011/12/c-performance-vs-delphi-performance.html
      Хотя и бредово сравнивать VS 2010 с оптимизацией под 64-битные приложения с производительностью Delphi 7 от 2001 года, но результат вполне адекватен.

      Вообщем этот разговор бессмыслен, так как во-первых я разрабатываю под обоими языками и могу реально сравнивать производительность/удобство разработки, во-вторых - для OpenGL приложений производительность упирается в GPU и применяемые технологии, а не в производительность кода (кроме редких случаев, требующих расчетов на стороне CPU), в-третьих - я не ставлю целью кого-то переубеждать, нравится С/С++ - используй на здоровье, хуже не будет :)

      Удалить
  3. Я не собирался с вами спорить.И хочу сказать спасибо за статьи,про такие тонкости в OpenGL ни где не находил по русски.Ваш пример писал на PureBasic,единственное не сразу понял как портануть
    glBufferData(GL_ARRAY_BUFFER, sizeof(GLFLoat)*3*Count, @VertexBuffer[0], GL_STATIC_DRAW);
    Только посмотрев как реализован этот метод на си понял.Почему здесь sizeof(GLFLoat)*3*Count нужно умножать на 3?Я указал просто размер массива в байтах 4 * Count.
    Да но,с размером дельфи проигрывает.Ваш пример в .exe весит 1.5 мега,такой же на C++ 7 килобайт.
    А какие игры вы писали?Хотел бы посмотреть.И вобше сотоваришей по OpenGL у меня нет.

    ОтветитьУдалить
    Ответы
    1. dzanis, на счет размера "sizeof(GLFLoat)*3*Count" - в статье этот момент детально описан, видно Вы все же копипастили а не читали :)

      Размер в байтах равен Количеству вершин * количество компонент/координат у каждой вершины * размер одной компоненты.
      У нас квадрат, у него 4 вершины. Каждая вершина имеет 3 координаты (X,Y,Z), каждая координата это GLFloat, который занимает 4 байта, таким образом, размер массива в байтах будет равен: 4 вершины * 3 координаты * 4 байта = 48 байт.

      На счет размера файла - для меня он не имеет значения, хотя на Делфи также можно писать 20кб приложения (DelphiXE, на старых версиях порядка 8-10кб), отказавшись от VCL и переписав создание окна на WinAPI, как это делается в С/С++, отказавшись от включения в проект библиотек времени выполнения и распространяя их как "Redistributable Package", отказавшись от 90% стандартных библиотек, и переписав все эти функции вручную, как это делают в С/С++.
      Просто не все понимают откуда этот размер берется, потому и возникают мифы об "эффективности" С/С++. К слову, простейший пример на OpenGL/С++, на WinAPI, с библиотекой GLEW и GLM у меня занимает 672кб, это куда больше тех 7кб, о которых Вы говорите.

      Так как я не люблю кидаться словами - посмотри эти демки:
      http://file.qip.ru/file/1o8lxjgt/stencil.html
      Каждая из них занимает всего 21-23кб, и это полноценные и самодостаточные Win32 приложения, не требующие распространения с ними никаких дополнительных библиотек.

      Удалить
  4. И правда я скопипастил, ведь Count это массив структур о_0 .Ни одна из демок незапустилась... Окна появляются с белым фоном,и у всех одна и та-же ошибка "Runtime error 216 0000..."
    У меня Win7
    OpenGL Vendor: NVIDIA Corporation
    OpenGL Renderer: GeForce 8400 GS/PCIe/SSE2
    OpenGL Version: 3.3.0
    OpenGL Shader: 3.30 NVIDIA via Cg compiler

    ОтветитьУдалить
    Ответы
    1. VertexBuffer: Array of TVertex - вот это массив структур, а Count - это количество элементов в этом массиве.
      Так как в одномерном массиве все элементы следуют друг за другом то я имею полное право формировать такой массив и передавать его по указателю на первый элемент.
      Хз как в бейсике, я на нем в последний раз писал в 95 году, в крайнем случае можешь переписать все на одномерные массивы, просто придется писать:
      VertexBuffer[Index*3+0]:=X;
      VertexBuffer[Index*3+1]:=Y;
      VertexBuffer[Index*3+2]:=Z;
      Где Index это номер вершины (от 0 до Count-1).
      Вообще будет тебе уроком, статьи для того и пишут чтобы их читали, а не копипастили код :)

      На счет демок - хз, у меня щас стоит ХР, не могу проверить поведение на Win7, но вообще очень странно. Эти три демки взяты с "www.sulaco.co.za", сейчас лень разбираться что именно там могло не понравиться в инициализации, темболее там проверяется буквально каждый шаг выполнения и в случае ошибки должно было появиться описание ошибки... Может позже гляну в чем проблема.

      Удалить
    2. P.S. Уточни, ты не смог запустить демки идущие к статьям или не смог запустить демки, которые я выложил в предыдущем посте?

      Удалить
  5. Не запускаются те что тут http://file.qip.ru/file/1o8lxjgt/stencil.html.Думаю не совместимось с windows 7. А демки к статьям запускаются.

    ОтветитьУдалить
  6. Да, цикл статей, как и блог, отличные. Реально формат хороший - коротко и доступно. Спасибо.

    Возник вопрос. Всё, что описано в уроке работает нормально,
    но вот я захотел поместить цвет(или не важно что) и координаты в один буфер,
    т.е. случай, когда координаты в буфере идут не последовательно.
    По логике, описанной в статье, отрисовать треугольник из такого буфера можно, если использовать соответствующие параметры при вызове функции glVertexPointer - т.е. шаг и начальное смещение. Но у меня при таком подходе треугольник рисуется не правильно, координаты из буфера берутся не верно. Собственно вопрос - в целом так можно делать ( и просто ошибка где-то у меня)? Или отрисовать из VBO не последовательные координаты таким образом не возможно? И второй момент - у вас в статье написано, что последний аргумент функции glVertexPointer задаёт начальное смещение данных в буфере, но в справках и других материалах, которые я находил, он описывается как указатель на первый элемент данных, лежащих в оперативной памяти, как я понял. В итоге не могу сообразить, что значит этот параметр, или его назначение зависит от контекста использования?

    Спасибо. С уважением, Евгений.

    ОтветитьУдалить
    Ответы
    1. Пожалуйста.
      На счет вашего вопроса - этот момент описан во второй половине 5-й части уроков:
      http://vbomesh.blogspot.com/2012/02/vbo-opengl_269.html
      Если вкратце - это можно сделать, видно у вас была ошибка. Посмотрите 8-ю демку и листинги 30-34.

      На счет второго вопроса, о последнем параметре glVertexPointer - это описано там же. Существует три способа представления данных для VBO, первый - каждый вершинный атрибут находится в своем буфере, в этом случае последний параметр указывает на начало буфера. Второй вариант - буферы вершинных координат "склеены" в один буфер, тоесть в одном буфере вначале идут все координаты вершин, потом сразу за ними идут все текстурные координаты, затем нормали и т.д.
      К примеру, у вас есть 10 вершин и три атрибута (позиция, цвет, нормаль), при этом позиция это 3 флоата (x,y,z), цвет - 4 флоата (r,g,b,a), нормаль - 3 флоата.
      Соответственно:
      размер первого "пакета" (позиция) будет равен: 10 вершин * 3 флоата * 4 байта = 120 байт.
      размер второго пакета (цвет) будет равен 10 вершин * 4 флоата * 4 байта = 160 байт.
      размер третьего пакета (нормали) равен: 10 вершин * 3 флоата * 4 байта = 120 байт.
      Тогда буфер будет иметь вид:
      [===Verices 120b===][===Colors 160b===][===Normals 120b]
      В этом случае мы должны описать эти атрибуты как:
      glVertexPointer(3, GL_FLOAT, 0, PGLUINT(0) );
      glColorPointer (4, GL_FLOAT, 0, PGLUINT(120) );
      glNormalPointer(GL_FLOAT, 0, PGLUINT(120+160) );
      Тоесть последний параметр задает смешение от начала буфера, где начинаются нужные данные. Формально, для glVertexPointer, это смещение совпадает с началом буфера, но никто не запрещает перестроить буфер таким образом:
      [===Colors 160b===][===Normals 120b][===Verices 120b===]
      В этом случае указатели уже будут такими:
      glVertexPointer(3, GL_FLOAT, 0, PGLUINT(160+120) );
      glColorPointer (4, GL_FLOAT, 0, PGLUINT(0) );
      glNormalPointer(GL_FLOAT, 0, PGLUINT(160) );
      Как видишь, теперь glVertexPointer уже не указывает на начало буфера.

      Есть еще третий вариант представления буферов, смешанный (чередующийся/interleaved), в том случае все вершинные атрибуты для каждой вершины следуют последовательно друг за другом:
      X,Y,Z,Nx,Ny,Nz,Cr,Cg,Cb,Ca,Ts,Tt, X,Y,Z,Nx,Ny,Nz,Cr,Cg,Cb,Ca,Ts,Tt…
      В этом случае мы так же указываем последним параметром смещение от начала буфера, к примеру для нормалей из строки выше это будет 12 байт, и вдобавок указываем сколько байт нужно пропустить до следующей порции нормалей. В примере выше это будет 36 байт, которые и указываются третьим параметром в gl*Pointer (для нормалей - 2-м параметром).

      Вообщем все это подробно с примерами расписано в 5-й части уроков.

      Удалить
    2. Да, спасибо. Действительно, я не правильно задавал количество байт, определяющих шаг. Сейчас всё работает, но опять странно - я указал не количество байт до новой порции вершин, а размер полного блока атрибутов вершин, примерно так:

      typedef struct {
      GLufloat x,y,z;
      GLufloat r,g,b,a;
      } Vertex;

      ...

      VertexPointer(3,GL_FLOAT,sizeof(Vertex),0);

      Вроде работает правильно.

      Удалить
    3. Ну правильно, если считать с начала блока нормалей до следующего блока нормалей то это как раз и будет полный размер блока.

      Удалить
  7. Всё, понял. В приведенном в комментарии примере (последний описанный способ расположения данных) компоненты Cr,Cg,Cb,Ca занимают по 1-му байту, тогда всё сходится :). Просто из описания предыдущих способов, я решил, что они занимают по 4-ре байта каждый и тогда размер полного блока атрибутов будет не 36, а 48 байт.

    ОтветитьУдалить
    Ответы
    1. Я просто поленился и скописастил этот кусок из статьи :)
      Все верно, если брать как флоаты, то там должно быть 48 байт, я собственно и думал о флоатах когда это писал, но в статье, для большей наглядности, я решил скомбинировать разные типы данных, и успешно забыл об этом :)

      Удалить
  8. Замечательные статьи! Жаль, демки не качаются, очень помогли бы!

    ОтветитьУдалить
    Ответы
    1. Недавно google обновил свой google sites, где у меня были все архивы загружены, после этого архивы где-то пропали :(
      Пока загрузил архивы демок сюда:
      https://drive.google.com/drive/folders/1bxiYVXV5TqL4Gvh2xyHMzM6hAi6JO0mt?usp=sharing

      Удалить
    2. СПАСИБО! Скачал, буду изучать. Пока сделал на основе частей 1 и 6 без GLScene. Delphi для меня - хобби. Делаю простую CAD, единственный объект - линии. Все примеры рисуют треугольники, а мне надо линии. Попробовал, получилось. Одна линия - один VAO, пока. Вот только как привязать атрибуты линии не к каждой вершине, а ко всем сразу - не пойму.

      Удалить
    3. Да, GLScene там используется просто для создания окна, вместо нее можно использовать GLFW, GLUT, SDL и прочее.
      Если набор вершинных атрибутов везде одинаковый, то я бы рекомендовал иметь один большой буфер на всю геометрию, это намного эффективнее чем VAO на линию. Фактически при рисовании нужно будет лишь указать смещение в индексном буфере.
      Атрибуты задаются для каждой вершины, но есть один хак - если количество атрибут меньше количества вершин, то OpenGL просто дублирует атрибуты. Тоесть получается что координаты задаются для всех вершин, а остальные атрибуты только для первой. Но в этом случае я бы рекомендовал освоить шейдера.

      Удалить
  9. Да, использую шейдер для расцветки, шаблона и его масштаба (замена glLineStipple). Но только вот тормозит! Сделал так: класс ЛИНИЯ (от TObject), у каждой линии свои Vao, VboVertex, VboColor, VboPattern, VboFactor. Еще класс ЛИНИИ (от TObjectList), там все тривиально. У линии метод Draw простейший:
    if FUpdating then Exit;
    glBindVertexArray(FVao);
    glDrawArraysInstanced(GL_LINES, 0, LINE_VERTECES_COUNT, 1);
    glBindVertexArray(0);
    Запускаю приложение - съедает 16 МБ памяти. Добавляю 1000 линий - уже ест 150 МБ. Откуда? Если добавить еще 4000 линий (5000 всего) - 685 МБ! Если закомментировать Draw, то на каждые 1000 линий ест только 22 МБ. Приложение 32 бит, естественно тормозит. ОЗУ 8 ГБ, видео встроенное в Core i7. Это при том, что рисуя через обычный glBegin летает при 15 тыс. линий. В чем дело не понятно. Конечно, все линии надо сливать в одни буферы - планировал в дальнейшем, но не думаю что причина тормозов в этом, ведь glBegin летает, а там все линии постоянно через шину передаются. В варианте через glBegin даже пробовал вводить случайный цвет при каждом вызове - летает, зараза, а через буферы тормозит!

    ОтветитьУдалить
  10. Что есть "линия"? Это отрезок из двух точек? или это линия из 1000 вершин/отрезков?

    Фактически это вопрос сколько раз вызывается glBind+glDraw, 1 раз или 5000 раз. Если 5000 раз - значит вы все делаете неправильно. Как правильно - спросите на форуме, в рамках блога это несколько неудобно объяснять.

    ОтветитьУдалить
    Ответы
    1. Да, "линия" - это отрезок из двух точек. Да, вызывается 5000 раз. Да, неправильно. Смущает расход памяти и тормоза. При том, что без VAO и VBO, т.е. с glBegin - все летает и на 15 000 линий. Пока решил собрать все линии в один VAO, чтобы вызывать 1 раз glBind+glDraw. Хотя выше уже писал, что закомментировав отрисовку получаю расход памяти 22 МБ на 1000 линий, при том что в варианте с glBegin всего 2 МБ на 5000 линий. Что-то странное.

      Удалить
    2. Вы должны понимать что при использовании glBegin/glEnd ничто нигде не хранится, вы каждый кадр на лету формируете "новый" список геометрии для отрисовки, именно потому оно потребляет так мало памяти.
      При использовании VBO - у вас одна копия геометрии хранится в системной памяти, вторая - в видеопамяти + накладные расходы на хранение хэндлов всех буферов и прочего, хранящегося в ваших классах "Линии". Так же, возможно, вы смотрите потребление памяти в отладочном режиме, попробуйте собраться в "релиз". Так же, возможно, TObjectList резервирует больше памяти чем имеется данных. Вообщем это не курс делфи, чтоб объяснять как управлять памятью приложения, и "гадать на кофейной гуще" что у вас там в коде может потреблять память мне не хочется.

      По поводу второго вопроса - "почему тормозит", я уже объяснил - так как вы сделали - делать КАТЕГОРИЧЕСКИ нельзя, иначе.... - будет тормозить.
      Суть VBO в том, чтоб хранить все данные в видеопамяти для быстрого доступа к ним, тоесть нет необходимости каждый раз загружать их в видеопамять для отрисовки, но само переключение этих буферов (glBind*) + вызов команды glDraw * - это очень дорогая операция (микро- и нано- секунды, но если это делать 5000 раз - уже будут миллисекунды). Фактически, при использовании glBegin/glEnd вы вызываете эти команды один раз, а в вашем коде с VBO они вызываются 5000!!!! раз, мало того - вы еще и умудрились туда запихнуть инстансинг, что вообще бред. При таком подходе оно гарантированно будет тормозить. Как правильно - один буфер на всю геометрию, в идеале - отрисовывать всю геометрию за один вызов glDraw*.

      Удалить
    3. Спасибо за диалог, это помогает. У меня glBegin/glEnd вызывается для каждой линии и все летает. Поля каждой линии - 73 байта, пусть весь класс 200. На 1000 линий 200 кБ, откуда берется 150 МБ загадка. С другой стороны, провел эксперимент - сделал один VAO с VBO для вершин VboVertex и атрибутов: VboColor, VboPattern, VboFactor и слил туда все свои линии. При 15 тыс. линий все взлетело. Расход памяти всего 20 МБ. Поэтому тормоза списываю на расход памяти. Если Вам не трудно прошу показать как сделать так, чтобы атрибуты вершин Color, Pattern, Factor указывать не для каждой вершины, а для пары? Сейчас они заданы так:
      glVertexAttribPointer(VertexLocation, 2, GL_FLOAT, False, 0, nil); // 2 - т.к. 2 компоненты в вершине
      . . .
      glVertexAttribPointer(ColorLocation, 3, GL_FLOAT, False, 0, nil); // т.к. 3 компонента в цвете
      . . .
      glVertexAttribIPointer(PatternLocation, 1, GL_UNSIGNED_INT, 0, nil); // т.к. 1 компонент в шаблоне
      . . .
      glVertexAttribPointer(FactorLocation, 1, GL_FLOAT, False, 0, nil); // т.к. 1 компонент в множителе шаблона
      При отрисовке:
      glDrawArrays(GL_LINES, 0, 2 * FLines.Count);
      Это работает, если массив атрибутов сделан на каждую вершину, а надо - на пару. Если нужен весь код - вышлю. Спасибо.

      Удалить
    4. greencad, вы вообще читаете что вам пишут? "Поэтому тормоза списываю на расход памяти" - я не могу вежливо прокомментировать это.... Выше я написал откуда тормоза и с памятью это никак не связано.
      Откуда у вас перерасход памяти в приложении - это исключительно ваша проблема, не относящаяся к OpenGL, отлаживать ваше приложение у меня нет никакого желания, уж извините. Хотите чтоб вам это разжевали - идите на какой-то из специализированных форумов по делфи, там вам расскажут как профилировать свое приложение. Аналогично и о видеопамяти, для профилирования OpenGL существуют свои средства, и обзор их, как и объяснения на что тратится видеопамять, помимо явно заданных буферов - выходят далеко за рамки этих статей. Просто примите как факт что буфер на 5000 вершин это очень мало, сколько бы вы атрибутов не использовали.

      На счет атрибутов "на пару вершин" - это можно сделать только используя glVertexAttribDivisor, и только для конкретно этого случая. Если в двух словах - это позволяет указать сколько раз нужно повторять значение атрибута, к примеру, укажете 2 раза, значит один вершинный атрибут будет использоваться для пары вершин. НО! Описанный вами "перерасход памяти" не существенный (36кб на все данные), потому нет никакого смысла в "оптимизации" количества атрибутов для такого объема данных. Получить какой-то профит от этого можно только в комплексе с другими задачами, такими как инстансинг с динамическими данными, за счет экономии на объеме обновляемых за кадр данных. Но даже тут, заметить разницу можно только на БОЛЬШИХ объемах данных, это сотни мегабайт обновляемых данных за кадр.

      Удалить
    5. Набросал на коленке пример рисования 100000 разноцветных линий:
      https://drive.google.com/file/d/15ZAIMkiM5cwf5ymQsudlYbPRHij2GHuZ/view?usp=sharing
      Каждый отрезок имеет свой цвет, каждая синусоида это 5000 отрезков, всего таких синусоид 20 штук.
      Потребляет 32мб памяти, так как тянет за собой кучу библиотек из фреймворка, на данные приходится примерно 2мб.
      На моем старом металлоломе - 450фпс. Хз будет ли оно быстрее glBegin/glEnd, так как в драйверах это очень хорошо оптимизировано для CADов.
      Реализовал динамическое добавление/обновление данных, но только для примера.
      Пример сделал с использование OpenGL 2.1 (VBO+Shader), чтоб было в тему этого цикла статей, так что работать должно на любом металлоломе.

      К примеру приложил сорцы только непосредственно рендера линий, так как там много зависимостей на движок.

      Удалить
    6. Спасибо Вам за участие. Позволю себе еще немножко прокомментировать. Как только я свалил все свои линии в один буфер – перерасход исчез, когда же буферы были на каждую линию перерасход был. При этом никаких выделений памяти без освобождения нет (проверил в какой уж раз), само приложение простое, как раз для теста и создано было. Ваш пример с разноцветными линиями запустил, на моем встроенном в Core i7 видео показывает FPS=1280 в оконном режиме и 198 при развернутом окне. Но там от каждой линии видно всего пару пиксел, поэтому я собрал из Ваших классов VCL-приложения, где квадрат 50х50 заполняется линиями. Статистика такая (кол. линий/FPS в окне/FPS в развернутом окне): 20/3500/370; 20000/59/69; 100000/14/12. Эти приложения положил сюда (если вдруг кому будет любопытно взглянуть) https://yadi.sk/d/r4Xitaoof_8lMA .
      Вот уже на 20 тыс. линий хорошо заметно как тормозит панорамирование. Здесь пытаюсь получить совет https://gamedev.ru/code/forum/?id=255047 .
      Еще раз хочу сказать Вам спасибо и за общение и за статьи, без них я бы разбирался на порядки дольше.

      Удалить
    7. https://drive.google.com/file/d/1jihF4Djs4XAZBREFv7bjqOI_-GQyPiuG/view?usp=sharing
      Там три демки, с 100к линий, с 20к и с 5к.
      100к у меня уже упирается в филрейт, потому фпс проседает до 300, при 20к и 5к - уже упирается в CPU, но в обоих случаях фпс более 1000 кадров в секунду.
      Если при переходе в полноэкранный режим фпс падает, значит упираетесь в филлрейт и с этим уже ничего не поделать. У меня растеризатор загибается на 20к линий (фпс в полноэкранном режиме снижается до 500 кадров с 1000).
      Единственное что могу порекомендовать - проверьте чтоб блендинг был выключен, так же думаю тест глубины и запись в буфер глубины можно отключить, это освободит вам немного ресурсов.

      Удалить
    8. Ради интереса посчитал количество растеризируемых пикселей в этом примере на 100к линий - примерно 33 миллионов пикселей в оконном режиме,это примерно 8к разрешение) В полноэкранном - диких 133 миллиона.
      Вы уверены что у вас glBegin/glEnd способно вывести такое количество линий с приемлемым большим фпс?

      Удалить
    9. Спасибо. Мои результаты (в окне, конфиг.: Win7 х64, ОЗУ 8Гб, видео встроенное в Core i7, монитор 1920x1080): 5к - 276; 20к - 72; 100к - 16. Скрины здесь https://yadi.sk/d/YhZivIdxhxecXw .
      На мой взгляд качественно не отличается от того что приводил выше.
      Здесь исходники, exe и скрин через glBegin/glEnd https://yadi.sk/d/UZV0_9NKYur6LQ .
      Результат (в окне): 20к линий, FPS = 60. Т.е. буферы дают прирост 20%, на мой взгляд скромно, это и смущает.

      Удалить
    10. Есть несколько боттленеков, первый - CPU, если очень много раз вызывать glBind+glDraw то можно в него упереться. Второй - пропускная способность шины., с этой проблемой сталкиваешься при попытке загрузить большое количество данных на GPU, к примеру - при использовании glBegin/glEnd. Но сейчас шина позволяет передавать минимум 2Гб данных в секунду, в то время как у нас на 100000 линий требуется всего 4мб данных, тоесть даже glBegin/glEnd тут бы показал 500фпс. Использование VBO позволяет устранить этот боттлнек, все данные уже в видеопамяти, потому шина свободна.
      Дальше уже идут несколько типов боттлнеков на GPU - по количеству инструкций, по количеству выборок из текстуры, по работе с памятью, по растеризации и по филрейту. Где-то тут мы упираемся в боттлнек. У моей старенькой GT750Ti филрейт всего 16 гигапикселей в секунду, в предыдущем сообщении я приводил цифры - примерно 33 миллиона пикселей на 100к линий, тоесть максимальный теоретический предел на моей карточке 485 фпс, я выжал 320 фпс за полный цикл рендеринга, что в целом неплохо (константное время тратится на обработку сообщений, вычисление времени, бинды буферов и прочее).

      Что же касается скорости работы автокада - рендерить картинку 500 раз в секунду нет необходимости, рендеринг выполняется один раз в текстуру, и потом ты эту текстуру показываешь на экран хоть миллион раз, обновляется картинка только при изменении данных или вращении. Так же есть смысл поработать с иерархией элементов, 100к линий это 100% заполнение экрана в разрешении 8К, врядли такое кому-то нужно (а кому нужно, то там уже NVidia Quadro стоит). Часть элементов гарантированно будет занимать на экране либо область меньше пикселя либо будут за пределами экрана. В обоих случаях они не будут участвовать в растеризации что даст вам несколько лишних фпс (вспомните синусоиды из предыдущей демки). Так же, выкинув эти элемента из цикла рисования, можно сэкономить время GPU, но оно даже для 100к линий не существенное. Вообщем пробуйте, удачи.

      Удалить