Форум по теме | Размещение рекламы | Ссылки | Обратная связь |
Вы находитесь на
|
![]() |
Загружать шейдеры в видеокарту можно "вручную" или с помощью юнитов, упрощающих загрузку (например, shader.pas+dglOpengl.pas, движок eXgine, пример glsl_soulchild.pas из dglsdk и т.п.).
После загрузки шейдера у вас появляется ссылка на этот шейдер, и вы можете включать/выключать работу шейдера по своему усмотрению. Можно провести аналогию с glEnable(GL_TEXTURE_2D) / glDisable(GL_TEXTURE_2D). Вы включаете и выключаете шейдер: glUseProgramObjectARB( ССЫЛКА_НА_ШЕЙДЕР ). Если в качестве ссылки на шейдер передать число 0, то шейдер отключится.
Об использовании шейдеров и типах переменных в GLSL есть хорошая статья на gamedev.ru. Особое внимание хочу обратить на переменные типа uniform и attribute. Uniform-переменные вы можете передать в шейдер до начала блока glBegin/glEnd, а attribute-переменные можно задавать внутри блока glBegin/glEnd. В примере со скелетной анимацией я сначала передаю массив глобальных матриц как uniform, а индексы костей передаются внутри блока glBegin/glEnd для каждой вершины (attribute).
Для передачи массива из тридцати двух матриц 4x4 я воспользовался функцией glUniformMatrix4fv(shader_boneMat, 32, false,@skelState[i].shaderAbsoluteMat). Здесь:
shader_boneMat - ссылка на массив с матрицами
в шейдере
32 - количество передаваемых матриц
false - при передаче матрицы не транспонируются
@skelState[i].shaderAbsoluteMat - указатель на массив с матрицами
для i-того персонажа
С помощью функции glUniformMatrix4fv данные из Delphi - массива
shaderAbsoluteMat: packed array[0..31] of TMatrix4x4raw;
были переданы в GLSL - массив
uniform mat4 boneMat[32];
Помимо цвета, нормали и текстурных координат можно задавать собственные атрибуты вершин. В случае со скелетной анимацией можно задать номер кости, к которой прикреплена вершина:
glVertexAttrib1fARB(shader_boneIndex, номер_вершины);
glVertex3f(coord[0], coord[1], coord[2]);
Здесь:
shader_boneIndex - ссылка на переменную
типа attribute
номер_вершины - число, которое нужно передать в шейдер
Но откуда взять ссылку на переменную, находящуюся внутри шейдера? Эти ссылки получают при загрузке программы, после того как шейдер загрузится в видеокарту:
shader_boneMat := glGetUniformLocationARB(po,
PGLcharARB(PChar('boneMat')));
shader_boneIndex := glGetAttribLocationARB(po, PGLcharARB(PChar('boneIndex')));
С помощью этих двух строчек мы получим ссылки на переменные, которые в шейдере выглядят следующим образом:
uniform mat4 boneMat[32];
attribute float boneIndex;
Вершинные шейдеры выполняются для каждой вершины. Поэтому, например, вы можете сделать пульсацию 3D поверхности по синусу: для этого достаточно прибавлять в вершинном шейдере к координате gl_Vertex нормаль, домноженную на синус от времени, а полученный результат отправлять в gl_Position. Помимо вершинных шейдеров существуют ещё и фрагментные, которые выполняются уже для пикселей. В примере Parallax Mapping перед выводом пикселя на экран его текстурные координаты модифицируются в зависимости от положения камеры, благодаря чему появляется эффект объёмной картинки.
Использование вершинных шейдеров для ускорения расчётов
Некоторые функции выполняются
на процессоре видеокарты (GPU) особенно быстро. Например, к таким функциям
относится умножение вектора на матрицу. Практически все расчёты, связанные
со скелетной анимацией, можно перенести с CPU на GPU. Но здесь важно учитывать
следующий эффект: если переусердствовать нагружением видеокарты различными
шейдерами, то может возникнуть ситуация, при которой CPU простаивает в
ожидании перегруженного шейдерными задачами GPU.
Изменения в версии 1.2 1. Чтобы шейдер работал на видеокартнах ATI, строчка float texNum2 = floor(texNum*255-1+0.001); была заменена на float texNum2 = floor(texNum*255.0-1.0+0.001); 2. По совету CyberZX чтобы уменьшить число умножений теперь рассчитывается общая матрица трансформации вершины: (http://www.gamedev.ru/code/forum/?id=56039) 3. Индексы костей и текстур по прежнему передаются в диапазоне 0..1, так как если передавать числа 0..255, то возникает странное падение скорости на 30 FPS.
Использование VBO для ускорения рендеринга модели
Всем хорошо известен
способ по выводу моделей с помощью связки glBegin/glEnd, когда наша программа
посылает данные в видеокарту многочисленными вызовами glTexCoord2f, glColor3f,
glVertex3f и glNormal3f. В таком случае 3D модель из памяти CPU (Central
Processing Unit) отправляются в память GPU (Graphics Processing Unit),
что занимает определённое время.
Современные видеокарты
могут хранить 3D модели прямо в своей памяти. Отрисовка модели из памяти
видеокарты происходит существенно быстрее, так как не тратится время на
вызовы glTexCoord2f, glVertex3f и подобных функций, требующих времени
на копирование данных из одной памяти (CPU) в другую (GPU). Осуществить
ускоренную работу видеокарты позволяют Vertex Buffer Objects (VBOs).
В данной версии имеет место многократная трансформация повторяющихся вершин. Работа над устранением этой проблемы может дать ощутимый прирост скорости.
Совместное использование VBO и шейдеров
Максимальный прирост
производительности дают VBO с полностью статическими моделями: модели
один раз загружаются в память видеокарты сразу после запуска программы.
Под воздействием шейдеров статическая модель "оживает" непосредственно
внутри видеокарты. В таком случае у нас есть два источника прироста производительности:
VBO в режиме вывода статических моделей и быстрое перемножение вершин
на матрицы трансформации костей внутри шейдера.
Выбор формата для хранения 3D моделей в памяти видеокарты
В описании к функции
glInterleavedArrays сказано, что "For some memory architectures this
is more efficient than specifying the arrays separately". Поэтому
в целях оптимизации, а заодно и упрощения исходного кода, рендеринг в
VBO режиме будем производить через функции glInterleavedArrays и glDrawArrays.
Для начала нам необходимо
выбрать формат для хранения данных 3D модели в памяти видеокарты, который
поддерживается функцией glInterleavedArrays. В документации к OpenGL перечислены
следующие форматы: GL_V2F, GL_V3F, GL_C4UB_V2F, GL_C4UB_V3F, GL_C3F_V3F,
GL_N3F_V3F, GL_C4F_N3F_V3F, GL_T2F_V3F, GL_T4F_V4F, GL_T2F_C4UB_V3F, GL_T2F_C3F_V3F,
GL_T2F_N3F_V3F, GL_T2F_C4F_N3F_V3F, or GL_T4F_C4F_N3F_V4F.
Наиболее подходящим
является формат GL_T4F_C4F_N3F_V4F, так как он позволяет хранить для одной
вершины наибольшее количество данных:
У вас может возникнуть вопрос, зачем нам V4F (x,y,z,w) вместо V3F (x,y,z) и четыре координаты текстуры T4F вместо двух T2F, да ещё и цвет C4F. Ответ на этот вопрос состоит в следующем: "излишки" данных мы будем использовать для хранения весов (bone weight) и индексов костей, к которым прикреплена вершина. Посмотрим, где есть излишки, которые можно использовать для наших целей:
Всего получилоь 7 значений типа float, а это означает возможность прикрепить одну вершину как минимум к трём костям и задать текущую текстуру:
Все эти семь значений будет использовать в своей работе вершинный шейдер. В принципе можно ухитриться, и "упаковать" в эти 7 float-ов больше данных, а затем "распаковывать" эти данные в шейдере. Но для простоты рассмотрим случай, когда на вершину влияет не более трёх костей.
Загрузка данных VBO в память видеокарты
В загрузке данных VBO
нет ничего сложного: достаточно объявить в Delphi правильный тип, соответствующий
выбранному нами формату данных GL_T4F_C4F_N3F_V4F, и воспользоваться функциями
OpenGL для работы с VBO.
Для начала необходимо
создать VBO для хранения модели. Нам понадобится создать только один буфер:
glGenBuffersARB(1, @VBOlink);
Эта функция создаст один VBO и вернёт ссылку на этот объект в переменную VBOlink. Теперь созданный VBO необходимо сделать текущим:
glBindBufferARB(GL_ARRAY_BUFFER_ARB, VBOlink);
Нечто подобное мы уже наблюдали с функцией glBindTexture(GL_TEXTURE_2D, texID) для установки текущих текстур. Перед загрузкой модель нужно привести к тому виду, в котором она будетнаходиться в памяти видеокарты (GL_T4F_C4F_N3F_V4F). Тип данных для такой вершину на Delphi будет выглядеть так:
type TVertexForVBO=packed record ts,tt,tr,tq : GLFloat; r,g,b,a : GLFloat; nx,ny,nz : GLFloat; x,y,z,w : GLFloat; end;
После создания массива tempData из вершин TVertexForVBO и наполнения его описанными выше данными, можно приступать к загрузке данных в память видеокарты:
glBufferDataARB( GL_ARRAY_BUFFER_ARB, 3*length(myModel.poligons)*sizeof(TVertexForVbo), tempData, GL_STATIC_DRAW_ARB );
На вход функции glBufferDataARB передаются следующие параметры:
GL_ARRAY_BUFFER_ARB - тип загружаемых данных
3*length(myModel.poligons)*sizeof(TVertexForVbo) - объём загружаемых
данных
tempData - указатель на загружаемые данные (pointer)
GL_STATIC_DRAW_ARB - признак того, что загружаются данные статической
модели (без анимации)
В память видеокарты следует загружать модель, подвергнутую инверсным преобразованиям. На этапе отрисовки модели вершинный шейдер домножит вершины статической модели на матрицы и веса соответствующих костей, в результате чего мы получим анимированную модель.
Рендеринг VBO-модели
Загрузив один раз в
начале программы нашу модель в VBO, мы получаем ссылку VBOlink на этот
объект. Теперь вместо того, чтобы передавать в видео карту десятки тысяч
вершин, нормалей и текстурных координат достаточно одной только ссылки
VBOlink, и видео карта сразу же имеет доступ к 3D модели, так как она
уже находится в её памяти. Благодаря использованию функции glInterleavedArrays
рендеринг осуществляется предельно просто:
glBindBufferARB(GL_ARRAY_BUFFER_ARB, myModel.VBOlink);
glInterleavedArrays(GL_T4F_C4F_N3F_V4F, 0,nil);
glDrawArrays(GL_TRIANGLES,0,3*length(myModel.poligons));
Шейдеры для скелетной анимации
Индексы и веса влияющих
на вершину костей мы записывали в текстурные координаты R,Q и в цвет R,G,B,A.
Вершинный шейдер выполняется для каждой вершины, поэтому мы можем сразу
узнать данные о костях, к которым принадлежит данная вершина. Чтобы избежать
ошибок при отсечении с помощью функции floor дробной части от значений
индексов костей и текстур, к ним прибавляется 0.001 (надёжность данного
метода для разных видеокарт и драйверов пока неизвестна). Исходный код
вершинного шейдера:
uniform mat4 boneMat[32]; - матрицы всех костей varying float texNum; - индекс текстуры для данной вершины (тип varying сделан с целью передачи в фрагментный шейдер) void main(void) { float boneIndex[3]; - индексы трёх костей float boneWeight[3]; - веса трёх костей texNum = gl_Vertex[3]; - получаем индекс текстуры vec4 fixedTexCoord = gl_MultiTexCoord0; vec4 fixedColor = gl_Color; vec4 fixedVertex = gl_Vertex; vec4 finalVertex = vec4(0,0,0,1); Получаем данные (индекс, вес) для трёх костей: boneIndex[0] = floor(fixedTexCoord[2]*255.0+0.001); boneWeight[0] = fixedTexCoord[3]; boneIndex[1] = floor(fixedColor[0]*255.0+0.001); boneWeight[1] = fixedColor[1]; boneIndex[2] = floor(fixedColor[2]*255.0+0.001); boneWeight[2] = fixedColor[3]; Домножение на 255 служит для перевода из диапазона 0..1 в диапазон 0..255, где это требуется. Прибавление числа 0.001 сделано с целью избежать ошибок при отсечении дробной части. Все необходимые данные получены, можно восстанавливать нормальные OpenGL-евские значения: fixedTexCoord[2] = 0.0; fixedTexCoord[3] = 1.0; fixedColor[0] = 1.0; fixedColor[1] = 1.0; fixedColor[2] = 1.0; fixedColor[3] = 1.0; fixedVertex[3] = 1.0; mat4 finalMatrix = mat4(0); for (int i = 0; i < 3; i++) finalMatrix += boneWeight[i]*boneMat[int(boneIndex[i])]; Трансормация вершины с учётом весов и матриц трёх костей: finalVertex = finalMatrix*fixedVertex; finalVertex[3] = 1.0; Записываем итоговые значения (координата вершины, цвет, текстурные координаты): gl_Position = gl_ModelViewProjectionMatrix * finalVertex; gl_FrontColor = fixedColor; gl_TexCoord[0] = fixedTexCoord; }
Номер текстуры из вершинного шейдера через переменную texNum передаётся в фрагментный шейдер:
uniform sampler2D myTexture0; - ссылки на текстуры uniform sampler2D myTexture1; uniform sampler2D myTexture2; uniform sampler2D myTexture3; uniform sampler2D myTexture4; uniform sampler2D myTexture5; uniform sampler2D myTexture6; uniform sampler2D myTexture7; varying float texNum; - номер нужной текстуры (эту переменную
передаст сюда вершинный шейдер, т.к. тип varying) void main(void) { Перевод из диапазона 0..1 в диапазон 0..255: float texNum2 = floor(texNum*255.0-1.0+0.001); Отрисовываем пиксель, подставляя нужную текстуру: if (texNum2==0.0) gl_FragColor = texture2D( myTexture0, gl_TexCoord[0].st ); else if (texNum2==1.0) gl_FragColor = texture2D( myTexture1, gl_TexCoord[0].st ); else if (texNum2==2.0) gl_FragColor = texture2D( myTexture2, gl_TexCoord[0].st ); else if (texNum2==3.0) gl_FragColor = texture2D( myTexture3, gl_TexCoord[0].st ); else if (texNum2==4.0) gl_FragColor = texture2D( myTexture4, gl_TexCoord[0].st ); else if (texNum2==5.0) gl_FragColor = texture2D( myTexture5, gl_TexCoord[0].st ); else if (texNum2==6.0) gl_FragColor = texture2D( myTexture6, gl_TexCoord[0].st ); else if (texNum2==7.0) gl_FragColor = texture2D( myTexture7, gl_TexCoord[0].st ); }
Загрузка шейдеров.
Загрузка шейдеров осуществляется
с помощью удобного юнита shader.pas (delphi3d.net). Сначала идёт загрузка
вершинного и фрагментного шейдера, почле чего мы получим две ссылки типа
GLHandleARB:
vertex := LoadShaderFromFile('.\shaders\vertex.txt',
GL_VERTEX_SHADER_ARB);
fragment := LoadShaderFromFile('.\shaders\fragment.txt', GL_FRAGMENT_SHADER_ARB);
Затем мы объединяем шейдеры в одну шейдерную программу с помощью функции LinkPrograms из файла shader.pas:
FShaders[0]:=LinkPrograms([vertex,fragment]);
Далее необходимо получить ссылки для обращения к переменным шейдера:
shader_boneMat := glGetUniformLocationARB(po,
PGLcharARB(PChar('boneMat')));
for i:= 0 to 7 do
shader_myTexture[i] := glGetUniformLocationARB(po,
PGLcharARB(PChar('myTexture'+intToStr(i))));
Трансформация VBO-модели с помощью шейдеров в процессе рендеринга
Сначала необходмо выбрать
текущий шейдер. Так как мы загрузили шейдеры в нулевой элемент массива
FShaders, то включение этого шейдера будет выглядеть так:
glUseProgramObjectARB(FShaders[0]);
Текущая реализация шейдера
и программы позволяет отображать модели, в которых используется не более
восьми текстур. В далнейшем работу с текстурами можно усовершенствовать.
Работа с текстурами в шейдере организована следующим образом: перед отрисовкой
VBO-модели необходимо передать шейдеру список из восьми текстур и произвести
glBindTexture для этих восьми текстур из нашей программы:
for i:= 0 to 7 do glUniform1iARB(shader_myTexture[i],i); for i:= 0 to 7 do begin glActiveTexture(GL_TEXTURE0+i); glBindTexture(GL_TEXTURE_2D, i+1); end;
Далее следует копирование матриц трансформации из нашей программы на Delphi в шейдер:
glUniformMatrix4fv( shader_boneMat, 32, false,@skelState[i].shaderAbsoluteMat);
Теперь можно приступать к обычному выводу VBO-модели с помощью функций glBindBufferARB, glInterleavedArrays и glDrawArrays (см. выше).
Если вы захотите изменить
количество костей с 32 на другое число, то это необходимо сделать в трёх
местах:
1) в файле .\shaders\vertex.txt "uniform
mat4 boneMat[32];"
2) в файле .\_model.pas "shaderAbsoluteMat: packed array[0..31]
of TMatrix4x4raw;"
3) в файле .\unit1.pas "glUniformMatrix4fv( shader_boneMat, 32,
false,@skelState[i].shaderAbsoluteMat);"
При загрузке моделей Half-Life 2 SMD мне удавалось создавать 57 матриц костей (boneMat[57]), а при 64 программа выдавала шейдерную ошибку.
При том же количестве матриц количество передаваемых полезных данных можно увеличить вдве: строки матрицы 4x4 можно использовать для хранения перемещений (x,y,z) и кватернионов (x,y,z,w).
Полученные результаты
Совместное использование VBO и шейдеров позволило увеличить FPS в несколько раз. Возможные источники прироста производительности таковы:
Странным является то, что передача индексов костей и текстур, отнормированных в диапазон 0..1 даёт прирост производительности на 30fps.
Результаты тестирования (модель Half-Life 1 smd, 2215 полигонов):
Количество моделей | 1 | 2 | 3 | 4 | 5 | 30 | 100 | 300 |
FPS | 814 | 634 | 530 | 460 | 405 | 99 | 39 | 15 |
2215 * 100 = 221500 полигонов при 39fps
Для модели Half-Life 2 smd с 3852 полигонами:
Количество моделей | 1 | 2 | 3 | 4 | 5 | 30 | 100 | 300 |
FPS | 775 | 600 | 493 | 419 | 361 | 82 | 26 | 9 |
3852 * 100 = 385200 полигонов при 26 fps
Опубликование и использование любых материалов сайта разрешено только с указанием обратной ссылки на сайт. Любое нарушение авторских прав преследуется по закону РФ. |