1 Описание OpenGL 5
4 Методы GLSurfaceView 19
4.1 Методы GLSurfaceView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4.2 Реализация интерфейса EGLConfigChooser . . . . . . . . . . . . . . . . . . . . . 21
4.3 Реализация Renderer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.4 Стабилизация FPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.5 Реализация Activity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
8 Шейдеры 53
8.1 Немножко истории . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
8.2 Язык GLSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
8.2.1 Описание GLSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
8.2.2 Типы данных GLSL . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
8.2.3 Конструкторы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
8.2.4 Компоненты матриц и векторов . . . . . . . . . . . . . . . . . . . . . . . 59
8.2.5 Константы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
8.2.6 Структуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
8.2.7 Массивы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
8.2.8 Операнты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8.2.9 Функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8.2.10 Операторы ветвления и циклы . . . . . . . . . . . . . . . . . . . . . . . . 61
8.2.11 Прыжки и возвращаемые значения . . . . . . . . . . . . . . . . . . . . . 62
8.2.12 Разрядность (точность) данных . . . . . . . . . . . . . . . . . . . . . . . 63
8.2.13 Специальные ”встроенные” переменные . . . . . . . . . . . . . . . . . . . 64
8.2.14 Специальные ( встроенные ) константы . . . . . . . . . . . . . . . . . . . 65
8.2.15 Коммуникация Android-приложения и шейдера . . . . . . . . . . . . . . 66
8.2.16 Invariant переменные . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
8.2.17 Встроенные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
8.2.18 Тригонометрические функции . . . . . . . . . . . . . . . . . . . . . . . . 68
8.2.19 Экспоненциальные функции . . . . . . . . . . . . . . . . . . . . . . . . . 68
8.2.20 Общие функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
8.2.21 Геометрические функции . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
8.2.22 Матричные функции . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
8.2.23 Функции векторных отношений ( сравнений ) . . . . . . . . . . . . . . . 70
8.2.24 Функции выборки текстур . . . . . . . . . . . . . . . . . . . . . . . . . . 70
8.2.25 Немного об оптимизации шейдеров . . . . . . . . . . . . . . . . . . . . . 71
8.2.26 Структура программы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
12 Выгрузка шейдеров 95
СОДЕРЖАНИЕ 3
24 "Заглушки"атрибутов 113
Исправления приветствуются.
Все дальнейшее посвящено библиотеке OpenGL ES 2.0 под Android, и последующим версиям.
1 ОПИСАНИЕ OPENGL 5
1 Описание OpenGL
Что такое библиотека OpenGL ES 2.0?
На базовом уровне, OpenGL ES 2.0 — это просто спецификация, то есть документ, опи-
сывающий набор функций и их точное поведение. Производители оборудования на основе
этой спецификации создают реализации — библиотеки функций, соответствующих набору
функций спецификации ( W: ).
Вопрос не корректный.
Поэтому рассматривать версию стандарта меньше 2.0 на взгляд автора кроме как архео-
логам не имеет смысла. (стандарт версии 3.0 обратно совместим с 2.0)
1 ОПИСАНИЕ OPENGL 7
История ( немного ):
1
Cтек бывает двух типов, FIFO и LIFO. FIFO — акроним «First In, First Out» (англ.). Принцип «первым
пришёл — первым ушёл», LIFO — акроним «Last In, First Out» (англ.), обозначающий принцип «последним
пришёл — первым ушёл».
2 ПРИМЕР ПРОСТЕЙШЕЙ ПРОГРАММЫ С OPENGL 9
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
// режим смены кадров
// RENDERMODE_CONTINUOUSLY - автоматическая смена кадров,
// RENDERMODE_WHEN_DIRTY - по требованию ( glSurfaceView.requestRender(); )
setContentView(glSurfaceView);
} catch (RuntimeException e) {} // выводим окошко "увы, выше устройство слишком..."
public nRender() { }
Далее GLSurfaceView задает формат своей поверхности как RGB_565 ( 16bit цвет ).
На это стоит обратить внимание, подробности чуть ниже.
Теперь сама инициализация OpenGL:
...
EGL10 Egl = (EGL10) EGLContext.getEGL();
...
Инициализация EGL для конкретного дисплея. Возвращает версию EGL в массиве version[].
...
EGLConfig mEglConfig = mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);
...
3 EGL. ИНИЦИАЛИЗАЦИЯ OPENGL ES 13
Шаблон конфигурации
int[] configSpec = {
EGL10.EGL_RED_SIZE, 5,
EGL10.EGL_GREEN_SIZE, 6,
EGL10.EGL_BLUE_SIZE, 5,
EGL10.EGL_DEPTH_SIZE, 16,
EGL10.EGL_RENDERABLE_TYPE, 4,
EGL10.EGL_SAMPLE_BUFFERS, 1
EGL10.EGL_SAMPLES, 4,
EGL10.EGL_NONE
};
if(numConfigs <= 0) {
EGLConfig] configs = new EGLConfig[numConfigs];
egl.eglChooseConfig(display, configSpec, configs, numConfigs,mValue); // Полу
чаем список конфигураций.
// Теперь у нас есть заполненный массив конфигураций configs
} else {
//Конфигураций соответствующих шаблону не найдено.
}
Важно:
Когда более чем одна конфигурация буфера кадра соответствует шаблону, возвращается
список конфигураций.
EGL_NONE,
EGL_SLOW_CONFIG и
EGL_NON_CONFORMANT_CONFIG.
2. по EGL_COLOR_BUFFER_TYPE, в порядке:
EGL_RGB_BUFFER,
EGL_LUMINANCE_BUFFER ( монохромный ).
EGL_BIND_TO_TEXTURE_RGB,
EGL_BIND_TO_TEXTURE_RGBA,
EGL_CONFORMANT, EGL_LEVEL,
EGL_NATIVE_RENDERABLE,
EGL_MAX_SWAP_INTERVAL,
EGL_MIN_SWAP_INTERVAL,
EGL_RENDERABLE_TYPE,
EGL_SURFACE_TYPE,
EGL_TRANSPARENT_TYPE,
EGL_TRANSPARENT_RED_VALUE,
EGL_TRANSPARENT_GREEN_VALUE,
EGL_TRANSPARENT_BLUE_VALUE.
3 EGL. ИНИЦИАЛИЗАЦИЯ OPENGL ES 17
import javax.microedition.khronos.egl.*;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);
void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize,
int stencilSize) – установить конфигурацию с определенными значениями глубины цвета на
канал, depth-буфера и глубины буфера трафорета.
if (numConfigs <= 0) {
throw new IllegalArgumentException("No␣configs␣match␣configSpec␣RGB565"
);
}
}
EGLConfig] configs = new EGLConfig[numConfigs];
int[] num_conf = new int[numConfigs];
egl.eglChooseConfig(display, configSpec, configs, numConfigs,Value);
// получаем массив конфигураций
return configs[0]; // возвращаем конфиг
}
}
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
ВАЖНО: ПОТОКИ.
Класс Renderer выполняется в ОТДЕЛЬНОМ потоке, а не в UI или главном потоке,
для обеспечения лучшего быстродействия.
GLSurfaceView отрабатывает в потоке UI так как является View.
Пример
public boolean onTouchEvent(final MotionEvent event) {
glSurfaceView.queueEvent(new Runnable() {
public void run() {
render.onTouchEvent(event);
}
} );
return true;
}
4 МЕТОДЫ GLSURFACEVIEW 24
Refresh-Rate
. . .
Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).
getDefaultDisplay();
int refreshRating = display.getRefreshRate();
. . .
Я думаю, понятно, что делать FPS выше экранного refresh-rate, не имеет смысла.
Если требуется снизить FPS ( для экономии заряда или бывает, что full-refreshrate экрана
уже не тянет, а скачки с 35 до 60 выглядят хуже стабилизации на 30 ), то лучше выбирать
кратное соотношение, например 1/2 или 2/3.
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
void reqRend(){
mHandler.removeCallbacks(mDrawRa);
if (!RPause){
mHandler.postDelayed(mDrawRa, 1000 / FPS); // отложенный вызов mDrawRa
glSurfaceView.requestRender();
}
}
5 ОТСТУПЛЕНИЕ А: ДВИЖКИ, ОПТИМИЗАЦИЯ... 26
@Override
public boolean onTouchEvent(final MotionEvent event) { // передаем MotionEvent
event в поток Renderer
glSurfaceView.queueEvent(new Runnable() {
public void run() {
render.onTouchEvent(event);
}
});
return true;
}
@Override
protected void onPause() {
super.onPause();
glSurfaceView.onPause();
RPause = true; // флаг паузы
}
@Override
protected void onResume() {
super.onResume();
glSurfaceView.onResume();
RPause = false; // флаг паузы
reqRend(); // запускаем рендеринг
}
@Override
protected void onStop(){
super.onStop();
RPause = true;
this.finish();
}
5 Отступление А.
Про движки, оптимизацию и общие правила
+ конкретно про java под Android.
5.1 Лучшее – враг хорошего или будьте проще
Все начинающие знакомство с OpenGL на первых порах пытаются сделать универсаль-
ную обертку, движок/набор классов.
Это полностью тупиковая ветвь.
Запомните – ВЫ НИКОГДА НЕ ПРЕДУСМОТРИТЕ ВСЕГО. Это пустая трата времени.
Работайте по отклонениям.
Реализуйте только то, что реально необходимо в конкретном проекте.
Все равно всегда возникнет момент, который вы не предусмотрели.
5 ОТСТУПЛЕНИЕ А: ДВИЖКИ, ОПТИМИЗАЦИЯ... 27
Про оптимизацию:
Оптимизировать нужно только то, что реально влияет на производительность конкретной
задачи/приложения.
Допустим вы ускорили загрузку и инициализацию в два раза, потратив на это три месяца.
Раньше приложение грузилось за 0.8 секунды, а теперь стало за 0.4
То есть вместо того, чтобы сделать приложение быстрее, вы занимались хрен знает чем, что
ни один пользователь не разглядит.
Другое дело, если раньше загрузка и инициализация занимала пол минуты. Пользователей
это нервировало.
То есть оптимизируем дополнительно только то, что "критично", а не пытаемся сразу сделать
самый быстрый код везде.
Даже у больших компаний не хватает времени для оптимизации всего, что говорить про
инди или маленькие конторки.
Тут все скажут – это дураку понятно, но я реально встречал много случаев когда так и
происходило.
Программист вместо того, чтобы перейти к следующей задачи продолжал играть с предыду-
щей под предлогом оптимизации, котороя ей нафиг не нужна была.
Float – объект, соответственно у него есть ссылка и счетчик ссылающехся на него ( для
сборщика мусора )
Ссылка как минимум 32бит, счетчик как минимум 32бит.
То есть, чтобы записать 32 бита ( данных ), мы израсходовали 96. ( o_0 )
Далее еще Vertex[] model добавит свои 64.
То есть получается что Float x, y, z весит 352бита вместо 96бит.
Другой пример:
Vertex[3][10000]
Vertex[10000][3]
Мосты в США:
1. Титаны.
UE, FrostByte, GameBro, Q3,4,5∼ и т.д.
Плюсы:
100500 игр на них.
Реальная мультиплатформенность.
Скорость, стабильность.
Заточены под работу большой группы людей.
Очень дорого. Как сам движок так и поддержка.
Зато очень хорошо сокращают время на разработку, которая часто выходит дороже стоимо-
сти этих движков.
Маленьким конторам и инди обычно не нужно столько наворотов за такую цену.
И обычно даже движки ААА класса не без патологий, так как они тянут за собой много
хвостов.
2. 2D движки бесплатные.
Бесплатные – на безрыбье рак рыба.
Часть возможностей реализована через попу, часто создается впечатление что это курсовик
студента, который почему-то решил это допиливать.
Но не все так плохо.
Есть вполне годные под определенные задачи.
Обычно хоть и с доступными исходниками – разработчиков 1-3 человека.
Как заканчивается энтузиазм – опенсорс и через пол года...
3. 2D движки платные.
6 ОТСТУПЛЕНИЕ B: ПРИМИТИВЫ И ПРОЧЕЕ 30
4. Движки рекламные.
Когда привлечение пользователей и программистов не из-за качества или использования – а
из-за рекламы.
DarkBasiс,Unity3D и т.д.
5. Конструктуры.
Ну что про них сказать?
Реализовали разработчики – используйте.
Не реализовали – не используйте.
У продвинутых присутствует возможность расширения.
Выбор для тех кто слаб в программировании.
Выбор движка:
Когда машину ремонтируете, выбираете ключ под гайку или берете любимый ключ и хо-
дите вокруг машины, думая, что открутить?
Обычно разработчики подробно описывают возможности, так что подобрать то что надо не
составит труда.
Немного истории:
На заре становления графических ускорителей разные производители пытались использовать
некоторые другие примитивы, такие как квадратичные поверхности ( Nvidia NV1 ) и четы-
рёхугольники (Sega Saturn), что несомненно было бы шагом назад для всей компьютерной
графики.
Большая сложность моделирования, огромная сложность вычислений по тем временам ( чи-
пы 92-95года не могли аппаратно вычислять те уравнения с нужной точностью, что давало
огромные искажения – например при наложении текстур ), плохое качество картинки.
Но тут вмешалась фирма Microsoft, сделав API Dicert3D с поддержкой только треугольни-
ков ( и Quard соответственно ), что определило дальнейшие развитие GPU ( и слава богу
), да и остальное программное обеспечение того времени такое как 3D Studio ( еще без
MAX ) и LightWave 3D работали с треугольниками. В некоторых системах рендеринга до
сих пор используется большее количество примитивов ( в основном в совтверных, то есть
просчитываемых на не GPU а на CPU ), таких как бесконечная плоскость, идеальный шар (
овал ) и т.д.
2
Другие примитивы тоже задаются с помощью точек, их тоже называют по аналогии с Triangle – вершины
( Vertex ).
6 ОТСТУПЛЕНИЕ B: ПРИМИТИВЫ И ПРОЧЕЕ 32
Как помним из геометрии однозначно задать плоскость можно с помощью трех точек не
лежащих на одной прямой.
Теперь представим плоскость проходящую через вершины треугольника и ограниченную его
ребрами. Это и есть наш примитив Triangle.
В дальнейшем треугольные примитивы буду называть ”полигоны”, так как именно такое
обозначение будет встречаться почти во всех статьях и книгах.
GL_TRIANGLES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
В моих примерах примитив Line я не использую, так что детально его рассматривать не
буду ( так как не было цели написать библию OpenGL ES ).
Скажу только, что тут все аналогично полигону, только задается с помощью двух вершин
(точек).
Вообще, "строго"говоря нынешнюю 3D графику можно отнести к векторной, так как все
примитивы задаются с помощью векторов =).
Нормаль – это вектор перпендикулярный чему либо, в 3D графике в основном это вектор
перпендикулярный поверхности полигона.
6.2.1 Матрицы
В примерах я буду использовать матрицы размера 3x3 и 4x4.
Расписывать действия над матрицами и векторами не буду ( очень много ), тем более в
инете полно хороших статей ( например вот эта, советую освежить память - или наоборот,
узнать много нового =) ). Настоятельно не рекомендую читать википедию по этим вопросам.
( хотя можете попробовать )
6.2.2 Преобразования
Есть три основных преобразования:
Translate - перенос
Scale - скалирование
Rotate - поворот
Главное:
То что видно в GLSurface имеет координаты от -1 до +1 по X и Y осям.
Вне зависимости от реальных пропорций экрана.
Соответственно,
вычислить местоположения пиксела на экране можно таким способом:
𝑋𝑃 𝑖𝑥𝑒𝑙𝑆𝑖𝑧𝑒 = 2.0𝑓 /𝑋𝑆𝑐𝑟𝑒𝑒𝑛;
𝑌 𝑃 𝑖𝑥𝑒𝑙𝑆𝑖𝑧𝑒 = 2.0𝑓 /𝑌 𝑆𝑐𝑟𝑒𝑒𝑛;
Где 𝑋𝑆𝑐𝑟𝑒𝑒𝑛 и 𝑌 𝑆𝑐𝑟𝑒𝑒𝑛 – количество пикселов по оси на экране.
Соответственно,
перевести экранные (150, 100) в систему координат OpenGL:
𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛 = 𝑋𝑃 𝑖𝑥𝑒𝑙𝑆𝑖𝑧𝑒 * 150 − 1.0𝑓 ;
𝑌 𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛 = 𝑌 𝑃 𝑖𝑥𝑒𝑙𝑆𝑖𝑧𝑒 * (𝑌 𝑆𝑐𝑟𝑒𝑒𝑛 − 100) − 1.0𝑓 ;
Замечу, что в OpenGL ES 2.0 нет матрицы трансформации, камеры, мира – если мы сами
не зададим их и они нам нужны.
Соответственно нет установки проекций ( перспективной, ортогональной ) так как если мы
сами не преобразуем координаты в соответствии с перспективной, то используем ортогональ-
ную проекцию, и никто за нас не будет делать эти преобразования.
TEXTURE_2D
– обычная 2D текстура, представляет из себя обычную растровую картинку.
Например:
TEXTURE_CUBE_MAP
(TEXTURE_CUBE_MAP_POSITIVE_X,Y,Z, TEXTURE_CUBE_MAP_NEGATIVE_X,Y,Z)
CUBE_MAP на самом деле представляет из себя шесть 2D текстур и используется для
некоторых эффектов например таких как отражение, бесшовное небо и карта теней.
7 ТЕКСТУРЫ В OPENGL ES 2.0 38
Например:
Еще есть TEXTURE_3D, но, из-за того что это не входит в стандарт и доступно только с
помощью расширения, рассматривать тут не буду.
Скажу только, что эта текстура, у которой ещё есть глубина ( слои ) и она занимает соот-
ветственно X*Y*Z.
Важно:
Табличка:
Текстура LUMINANCE для программиста отличается от ALPHA только тем, на каком кана-
ле при выборке будут лежать данные.
При выборке (чтении данных с текстуры) в шейдере мы ВСЕГДА получаем четырехмерный
вектор V(r,g,b,a) и, в зависимости от типа текстуры, значения будут на определенном канале
( если канал не занят – значения будет 0.0 ).
Например, 16-битная ( разрядность текстуры пишут по сумме бит всех каналов) текстура за-
писана двумя байтами ( во 8 бит в каждом ), но она может быть как LUMINANCE_ALPHA
текстурой так и RGB и RGBA.
Например, текстура RGBA может быть в таком виде R5,G5,B5,A1, где цифры – число бит на
канал. А может быть и в таком: R4,G4,B4,A4, что тоже является 16-битной RGBA текстурой.
Для задания формата записи каналов текстуры в GLES20 используются следующие типы:
UNSIGNED_BYTE – для всех видов текстур, где размер всех каналов равен или кратен 8
битам (байту), например R8, G8, B8, A8 или L8, A8
Важно:
Я описал не все возможные форматы – это форматы гарантированно поддерживающиеся
всеми GPU по стандарту OpenGLES 2.0.
Есть еще много Vendor-зависимых форматов, как то например R10G10B10A2 т.д.
Но их лучше никогда не использовать если конечно вы хотите чтобы кто-то с другим GPU
смог запустить приложение.
7 ТЕКСТУРЫ В OPENGL ES 2.0 40
Конечно ваш GPU может поддерживать полную работу с NPOT-текстурами, но так как
это не входит в стандарт нельзя гарантировать, что это будет работать везде.
Так что если хотите использовать тайлинг или автоматическую генерацию мипмапов, то
придется использовать POT-текстуры.
Увы.
То есть 32битная текстура размера 1024 на 1024 занимает 4*1024*1024 = 4’194’304 байт.
4 мегабайта!
Вот куда уходит большая часть памяти в играх.
16-битная текстура 1024 на 1024 займет уже только 2мб, так что стоит подумать – стоит
ли использовать 32битные или нет.
Есть еще аппаратное сжатие текстур, которое обычно позволяет до четырех раз умень-
шить вес текстур, об этом ниже.
То есть даже если у вашего аппарата два Гб. оперативной памяти то одному приложению
будет доступно все равно не больше, чем размер кучи.
Но это нам не помешает, так как все текстуры ( и не только текстуры ) после загрузки
хранятся в памяти драйвера, на которую не действуют эти ограничения.
Ограничение конечно есть, но это обычно 256мб для старых систем и 1гб+ для новых.
Всё это из-за того что OpenGL ES 2.0 работает в режиме клиент-сервер ( как уже писалось
выше ), в данном случае клиент – это видеодрайвер, а сервер наше приложение.
Но, теоретически, ничего не мешает клиенту и серверу находится даже на разных машинах.
Понятия Видеопамяти в андроид как такового нет, так как GPU запрашивает нужную у
системы из общей памяти и, соответственно, затруднительно посмотреть сколько осталось
свободной.
Важно:
Так как клиент OpenGL не является частью нашей программы мы часто можем не узнать о
том, что операция закончилась некорректно.
Узнать об ошибке ( или наоборот хорошо закончившейся операцией ) с помощью функции
glGetError().
После каждой критической операцией, например такой как загрузка текстур, проверять зна-
чение возвращаемое glGetError() и, если оно не равно GL_NO_ERROR, то обрабатывать
ошибки.
В другом порядке может уже не хватить памяти для загрузки OpenGL ресурсов.
И лучше это делать в один поток, понятно по какой причине...
7 ТЕКСТУРЫ В OPENGL ES 2.0 42
Как видно из названия glGetIntegerv возвращает целое число в max[0], которое соответству-
ет максимальному размеру поддерживаемой текстуры.
Но всё же, без надобности лучше не применять текстуры максимального размера доступ-
ного на вашем GPU, так как не факт, что на других устройствах будет поддержка текстур
размером больше 1024.
Обозначения t и s судя по всему приняты для того чтобы не путать с другими данными.
Текстурные координаты могут быть как меньше размера текстуры ( 1.0 по каждой оси )
так и больше.
Если координаты меньше – текстура увеличивается, больше – текстура уменьшается.
Геометрия самого полигона не влияет на текстурные координаты.
Про то, что бывает, когда текстура меньше текстурных координат, чуть ниже.
В стандарте GLES определено как минимум 8 слотов для текстур фрагментного шейдера
и 0 ( ! ) текстур для вершинного.
Слоты для вершинного шейдера не поддерживает ни Tegra, ни Mali, поэтому для универ-
сальности придется отказаться от этой технологии =(. Но в защиту их будет сказано, что
они имеют другой функционал, который позволяет реализовать похожие эффекты.
7 ТЕКСТУРЫ В OPENGL ES 2.0 44
Выбрать текущий слот для работы ( подключению к нему текстуры, изменение параметров
текстуры ) можно с помощью процедуры:
GLES20.glActiveTexture(GLES20.GL_TEXTUREx);
Для удобства:
константы начиная с GL_TEXTURE0 идут по порядку,
так что GL_TEXTURE3 = GL_TEXTURE0+3;
Где:
первый параметр – тип текстуры,
второй – ссылка на текстуру.
Эта процедура прикрепляет текстуру к !текущему! слоту, который был выбран до этого
процедурой GLES20.glActiveTexture().
То есть для того чтобы прикрепить текстуру к определенному слоту нужно вызвать две
процедуры:
GLES20.glActiveTexture(Номер_Слота);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, ссылка_на_текстуру);
Важно:
Нельзя подключить одну текстуру одновременно к нескольким слотам.
Если вы ее переключили на другой слот и не установили текстуру для слота, куда она была
подключена раньше, попытаетесь из него прочесть – результат вплоть до падения драйвера.
Названия фильтраций:
GL_LINEAR – билинейная
GL_LINEAR_MIPMAP_NEAREST – билинейная с мипмапами
GL_LINEAR_MIPMAP_LINEAR – трилинейная
Мипмапинг ( MipMaps ):
ссылочка на описание
Скажу только что это улучшает качество фильтрации но увеличивает расход памяти. На
скорость может влиять как положительно так и отрицательно в зависимости от GPU.
Еще режим GL_REPEAT, когда одна текстура повторяется несколько раз называют тай-
лингом.
Текстурные параметры можно изменить в любой момент времени, а не только при ини-
циализации.
Как видите, параметры семплирования текстуры хранятся для каждой текстуры отдельно.
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture_id[0]);
// прикрепляем текстуру к текущему слоту. Первый параметр – тип текстуры, второй –
ссылка на текстуру.
// Внимание: помните какой сейчас текущий текстурный слот
bm.recycle();
/// ВАЖНО! Картинка в bm нам уже не нужна. Удаляем.
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.
GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.
GL_LINEAR);
// устанавливаем линейное сглаживание у текущей текстуры.
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.
GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.
GL_REPEAT);
// Устанавливаем тайлинг по осям S и T.
В библиотеке GLUtils нет функций для работы со сжатыми текстурами, увы, их придется
реализовывать самим.
Формат:
GLES20.glTexImage2D(enum target, int level, int internalformat,sizei width, sizei height, int
border, enum format,enum type, void *data);
Описание:
target – вид текстуры, например TEXTURE_2D
level – уровень мипмапкарты
internalformat – внутренний формат каналов, то есть как будем хранить (ALPHA, LUMINANCE,
LUMINANCE_ALPHA, RGB, RGBA)
format - формат самой текстуры. ( ALPHA, LUMINANCE, LUMINANCE_ALPHA, RGB,
RGBA )
type - тип текстуры (UNSIGNED_BYTE, UNSIGNED_SHORT_5_6_5, UNSIGNED_SHORT_4_4_4_4,
UNSIGNED_SHORT_5_5_5_1)
7 ТЕКСТУРЫ В OPENGL ES 2.0 50
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.
GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MAG_FILTER,GLES20.
GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S,GLES20.
GL_REPEAT);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T,GLES20.
GL_REPEAT);
Часть из них:
Tegra:
GL_EXT_texture_compression_dxt1
GL_EXT_texture_compression_latc
GL_EXT_texture_compression_s3tc
...
GL_OES_compressed_ETC1_RGB8_texture
Adreno:
GL_AMD_compressed_3DC_texture
GL_AMD_compressed_ATC_texture
...
GL_OES_compressed_ETC1_RGB8_texture
7 ТЕКСТУРЫ В OPENGL ES 2.0 51
PowerVR:
GL_IMG_texture_compression_pvrtc
...
GL_OES_compressed_ETC1_RGB8_texture
и т.д.
Пример сжатия:
Может случиться так, что определённое расширение могут реализовать несколько произ-
водителей.
В этом случае используется аббревиатура EXT.
В случае же, когда расширение одобряется консорциумом, оно приобретает аббревиатуру
ARB и становится стандартным расширением.
Обычно расширения, одобренные консорциумом, включаются в одну из следующих специ-
фикаций OpenGL.
Получить список всех поддерживаемых расширений вашего GPU можно функцией glGetString.
Для использования части расширений придется делать прямой импорт функций из биб-
лиотек драйверов, что возможно только под NDK,
Так что, если хотите использовать нестандартные функции – придется писать свою библио-
течку враппера на С++.
Хотя некоторые GPU умеют на лету сжимать текстуры – обычно это очень тяжелая опе-
рация и текстуры подгружаются уже сжатыми.
Существуют как отдельные утилиты, так и плагины для редакторов для работы со сжатыми
текстурами.
Скачать можно с сайтов производителей чипов.
Сжатые текстуры – это почти всегда главная причина разного кеша приложений под раз-
ные устройства.
Так что, если кто ковыряет кеши и не знает форматы – текстуры 99% хранятся в каком либо
формате производителя конкретного GPU.
8 Шейдеры
8.1 Немножко истории
Первоначально почти каждый разработчик писал свою версию рендера, как в случае с
Doom, Quake1, Duke nukem 3D и других.
Разработчик мог полностью управлять всеми аспектами рендеринга и в общем-то не было
никаких ограничений кроме производительности системы ( и рук).
Но, так как CPU тех времен достаточно плохо справлялись с рендеренгом в реальном
времени ( да и сегодняшние тоже не очень, так как специализированные процессоры за-
точенные под определенные задачи всегда быстрее универсальных ), а некоторые нужные
вещи как сглаживание текстур вообще превращало приложение в слайд-шоу, возникла по-
требность в отдельном аппаратном блоке разработанном именно под определенные расчеты.
Но, на самом деле, была ещё и обратная сторона переноса любых расчетов на GPU.
Вспомните первый Quake, когда там падаешь в воду, изображение искривлялось, или
CounterStrike где на софтварном движке при взрыве гранаты была не просто белая вспышка
– а пикселизация изображения, или Elite 3 с ее процедурными планетами – на которые
реально было посадить корабль и т.д.
8 ШЕЙДЕРЫ 54
Разработчики стали очень тесно зажаты в рамки API, будь то Glide, DirectX или OpenGL
(не путать OpenGL с OpenGLES).
На том же OpenGL приходилось делать кучу вызовов для отображения даже простейших
объектов и со сложностью росло их количество в геометрической прогрессии, даже позже
появившиеся буфера вершин и вызовов до конца не решали проблемы, так как нормально
работали только со статичной геометрией.
В первый раз в играх слово шейдер появилось в Quake3 в виде языка смешивания текстур
и аппаратной поддержкой в GPU Nvidia TnT и выше ( и аналогичных Radeon от ATI и S3 ).
Вспомните игры тех времен, например первый BloodLine, где все залито бамп-маппингом.
GPU того поколения были ещё с очень сильными ограничениями, но это уже был огром-
ный скачек к переходу от фиксированной логики к программируемой.
На нынешнее время стандарт OpenGL если не обгоняет DirectX, то точно не отстает, так
как поддержка новых возможностей за счет расширений появляется куда быстрее чем новая
версия DirectX.
У DirectX это выражается в очень плохой обратной совместимостью, так как версии
DX3D зачастую не совместимы между собой даже на уровне логики построения приложения,
перейти с DX7 на DX11 представляется мало возможным, так как придется переделывать
весь рендер ( а часто и все остальное ”блоки” ) с ”нуля”.
8 ШЕЙДЕРЫ 56
Для примера:
Пускай у нас есть 3D меш на 10к полигонов ( отдельных ),
дополнительные атрибуты вершин: цвет, нормаль и текстурные координаты.
Но спросите почему до сих пор не откажутся от таких явно устаревших и даже вредных
функций в API?
Все дело в обратной совместимости и в том, что под старые версии написано уже очень
много софта, и часто очень серьезного.
Была попытка отбросить хвосты начиная с версии OpenGL 3.0, но она так и закончилась
только сотрясением воздуха.
Так что, первой действительно удачным отбрасыванием хвостов можно считать только
OpenGL ES 2.0 и выше.
Существенно уменьшен размер API, убрано всё кроме самого необходимого, и всё что
можно перенесли на программируемую логику ( и программиста ).
OpenGL ES 2.0 основан на настольной OpenGL 2.0 с убиранием устаревших частей и до-
бавкой механизмов программируемой логики из OpenGL 3 и OpenGL 4 ( по словам разработ-
чиков, хотя на мой взгляд от настольного OpenGL 2.0 там осталось хорошо если процентов
15-20, а некоторые возможности вообще уникальны ).
OpenGLES 3.0 обратно совместим с OpenGLES 2.0, ваши программы разработанные под
2.0 будут себя прекрасно чувствовать и на 3.0
Язык для программирования логики работы GPU в OpenGL ES 2.0 – GLSL version 1.0.0
8 ШЕЙДЕРЫ 57
Вершинный шейдер
Выполняет обработку геометрии, то есть изменяет параметры вершины, такие, как пози-
цию, текстурные координаты, цвет вершин.
Также может выполнять вычисления освещения. Выполняется для каждой вершины (!).
Фрагментный шейдер
Выполняет обработку цветовых данных, полученных при рисовании треугольника.
Оперирует с текстурами и цветом.
Количество инструкций значительно ограничено производительностью. Выполняется для
каждого пикселя (!).
Целочисленные вектора
bool
bvec2
bvec3
bvec4
Матрицы
Пример
float u_Time; // скаляр с плавающей запятой
vec4 a_Position; // четырехмерный вектор с плавающей запятой
mat4 m_Projection; // матрица 4x4
ivec2 г_Offset; // целочисленный двухмерный вектор
8.2.3 Конструкторы
Конструкторы применяются для инициализации данных или преобразования типа.
Конструкторы данных
bool myBool = true; // объявление и инициализация boolean-переменной.
float myFloat;
vec2 myVec2;
myVec2 = vec2(myBool); // Ошибка! Разная размерность!
8 ШЕЙДЕРЫ 59
Конструкторы векторов
vec4 myVec4 = vec4(1.0); // myVec4 = {1.0, 1.0, 1.0, 1.0} , сокращенная запись
vec3 myVec3 = vec3(1.0, 0.0, 0.5); // myVec3 = {1.0, 0.0, 0.5}
vec3 temp = vec3(myVec3); // temp = myVec3
vec2 myVec2 = vec2(myVec3); // myVec2 = {myVec3.x, myVec3.y}
myVec4 = vec4(myVec2, temp, 0.0); // myVec4 = {myVec2.x, myVec2.y, temp, 0.0 }
Конструкторы матриц
mat3 myMat3 = mat3(
1.0, 0.0, 0.0, // первый столбец
0.0, 1.0, 0.0, // второй столбец
0.0, 1.0, 1.0 // третий столбец
);
Компоненты
Vec4[0] = Vec4.x = Vec4.r = Vec4.s
Vec4[1] = Vec4.y = Vec4.g = Vec4.t
Vec4[2] = Vec4.z = Vec4.b = Vec4.r
Vec4[3] = Vec4.w = Vec4.a = Vec4.q
Важно:
Нельзя смешивать символы из разных наборов, то есть выражение Vec4.xyba не допусти-
мо и вызовет ошибку.
Компоненты
vec2 myVec = vec2(0.0,1.0);
float myF0 = myVec[0]; // myF0=0.0
float myF1 = myVec[1]; // myF1=1.0
8.2.5 Константы
Константы объявляются с помощью ключевого слова ”const”.
Константы
const float zero = 0.0;
const float pi = 3.14159;
const vec4 red = vec4(1.0, 0.0, 0.0, 1.0);
const mat4 identity = mat4(1.0);
Константы
float f = 5; // Ошибка!
float f2 = 5.0; // правильно.
8.2.6 Структуры
Структуры – агрегативные типы данных как с Си.
Структуры
struct LightStruct
{
Vec4 position;
vec4 color;
float force;
} LightVar;
8.2.7 Массивы
Массивы
float floatArray[4]; // массив из четырех скаляров
LightStruct vecArray[8]; // массив из восьми структур LightStruct
8.2.8 Операнты
* Умножение
/ Деление
+ Сложение
− вычитание
++ Инкремент (пре или пост)
−− декремент (пре или пост)
= Присваивание
+=, —=, *=, /= арифметическое присваивание
==, !=, <, >, <=, >= Сравнение
&& логическое и
^^ Логическое исключающее или
|| Логическое или
Важно:
На некоторых GPU операции * и *= быстрее, чем / и /=
8.2.9 Функции
Пример
vec4 diffuse(
vec3 normal,
vec3 light,
vec4 baseColor)
{
return baseColor * dot(normal, light);
}
Важно:
Функции в GLSL не могут быть рекурсивными.
Пример
if(color.a < 0.25)
{
color *= color.a;
}
else
{
color = vec4(0.0);
}
for (int i = 0; i < 3; i++)
{
sum += i;
}
8 ШЕЙДЕРЫ 62
float myArr[4];
for (int i = 0; i < 3; i++)
{
sum += myArr[i]; // не применимо в GLSL
// Индексация только константами!
}
Важно:
Циклы не применять!
Разворачивайте и делайте через условные.
Один полупустой цикл может сделать из приложения слайдшоу.
( если без этого вообще не обойтись, например для костной анимации, очень осторожно,
перекрестись, и только в вершинном шейдере.
В фрагментном цикле приведёт к слайдшоу в любом случае)
Еще Важнее:
Циклы во фрагментном шейдере не применять.
Вообще.
( во всяком случае на нынешнем поколении мобильных GPU).
Ветвления очень аккуратно и если только без этого совсем не обойтись.
Даже одно ветвление может обойтись вам снижением на 20кадров от 45.
Старайтесь заменить ветвления на преобразования.
Или на два прохода.
Часто это быстрее, чем одно ветвление в фрагментной шейдере.
И главное:
discard просто убивает производительность почти на всех адаптерах, является тяжелей-
шей операцией.
Производители рекомендуют не использовать discard, в крайнем случае в этом участке
(!) не должно быть прозрачных элементов, но в любом случае потери производительности
будут большими.
Внимательные спросят: Как нечего-неделанье может что-то тормозить? А дело в том что
при вызове discard чипу нужно остановить ВСЕ процессоры и перестроить буфер глубины,
8 ШЕЙДЕРЫ 63
stencil, перестроить фрагмент и т.д. и только после этого можно продолжать работать. Это
относится к RowerVR, adreno, Mali и другим GPU со схожей архитектурой. На Tegra влияние
слабее.
Кстати, плавное ”сгорание” трупов монстров в Doom3 реализовано как раз через discard
=)
highp используется тогда когда нужны точные расчеты, например положение вершины.
mediump подходит например для текстурных координат.
lowp обычно используется для значений цвета, так как все цветовые каналы имеют
максимум 2^8, что полностью укладывается в lowp-точность.
lowp vec4 color = vec4(1.0,1.0,0.0,1.0); // lowp достаточно для хранения цвета
Задать точность для всех переменных определенного типа можно с помощью ключевого
слова precision
Precision
precision highp float; // теперь все float считаются highp
precision mediump int; // все int считаются mediump
\end{lstlising}
\begin{lstlisting}
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
В вершином шейдере:
В фрагментном шейдере:
bool gl_FrontFacing
– ( входящая ) фрагмент находится на лицевой ( true ) или обратной стороне полигона.
8 ШЕЙДЕРЫ 65
Важно:
Часть спецификаций ( и учебников ) описывает gl_PointCoord как mediump int gl_PointCoord,
что неверно.
Я думаю тут все понятно, gl_DepthRange хранит значение глубины, координаты экранные.
Если GPU не поддерживает highp во фрагментном шейдере, то точность будет mediump.
Важно:
Для тех кто был знаком с ”настольным” GLSL.
В OpenGL ES 2.0 нет больше никаких специальных переменных, которые были в на-
стольной версии, таких как атрибуты вершин,текстурных координат, униформ материалов и
источников освещения.
Специальные константы
const mediump int gl_MaxVertexAttribs = 8;
const mediump int gl_MaxVertexUniformVectors = 128;
const mediump int gl_MaxVaryingVectors = 8;
const mediump int gl_MaxVertexTextureImageUnits = 0;
const mediump int gl_MaxCombinedTextureImageUnits = 8;
const mediump int gl_MaxTextureImageUnits = 8;
const mediump int gl_MaxFragmentUniformVectors = 16;
const mediump int gl_MaxDrawBuffers = 1;
2. Uniforms (Униформы).
Используются как в вершинном так и фрагментной шейдерах.
Могут включать в себя любые данные ЕДИНЫЕ ДЛЯ ВСЕГО ОТРИСУЕМОГО МЕША.
Например время или положение и поворот всей модели, матрицы преобразований или
матрицу вида и т.д.
3. Varying ( Варинги =) )
Используются ТОЛЬКО для связи между вершинным и фрагментным шейдером.
Являются исходящими данными из вершинного шейдера.
На входе в фрагментный шейдер представляют из себя интерполированное значение меж-
ду вершинами примитива.
4. Sampler ( Текстура )
Могут использоваться как в фрагментном так и в вершинном.
Не все GPU поддерживают работу в текстурами в вершинном шейдере.
Важно:
Максимальное количество данных каждого типа имеет разное значение на разных GPU.
Важно:
Все значения для четырехмерных векторов.
Важно:
Последовательность записи специальных типов переменных: "специальность" точность
имя.
varying lowp vec4 color; // правильно
uniform mediump float time; // правильно
mediump uniform float time; // ошибка
8 ШЕЙДЕРЫ 67
2. Функции реализующие простые операции такие как Mix, Clamp, Abs и т.д.
В принципе эти функции легко реализуются пользователем – но встроенные имеют в
основном аппаратную поддержку ( а часто являются отдельной операцией GPU ).
Но в любом случае производитель лучше знает свое ”железо”, так что шанс написать
лучше реализацию очень мал.
Типы данных как float, vec2, vec3, или vec4 буду обозначать для краткости как genType,
матрицы mat2, mat3, или mat4 как mat.
Функция ”тяжелая”.
8 ШЕЙДЕРЫ 70
и
vec4 textureCube (samplerCube sampler,vec3 coord [, float bias] )
vec4 textureCubeLod (samplerCube sampler,vec3 coord, float lod)
пример
float f1, f2, f3, f4;
float c = f1+f2+f3+f4; // медленно
//-------------
float в = dot(vec4(f1,f2,f3,f4),vec4(1.0)); // быстро
Названия переменных:
Атрибуты буду называть с префикса "a"
Униформы c "u"
Варианты c "v"
и Семплеры с "s"или "t" (от Texture)
Для линии:
Вершинный шейдер отрабатывает два раза – для каждой вершины.
Далее фрагментный для каждой точки линии. Линии могут быть разной толщины.
Для точки:
Точка задается одной вершиной, соответственно вершинный шейдер запускается один раз,
далее фрагментный шейдер один или больше раз.
Точки могут быть разного размера и включать в себя несколько пикселей.
Приступим.
[fragment]
void main() {
gl_FragColor = vec4(1.0);
}
Входные данные:
Важно:
OpenGLES 2.0 может отрисовывать примитивы только заданные массивом или массивами
атрибутов вершин.
Другим способом задание примитивов невозможно.
Важно:
Рассмотрим:
aPosition будет равен ( -0.75f, -0.75f, 0.0, 1.0), так как vec3 автоматически дополнится до
vec4 значением 1.0.
Конечно можно определить aPosition как vec3, но тогда нам придется делать преобразо-
вание вручную gl_Position = vec4(aPosition, 1.0);
Действия GPU:
Для первой вершины aPosition будет равен (-0.75f, -0.75f, 0.0, 1.0) и соответственно
gl_Position = aPosition; // gl_Position будет равен vec4(-0.75f,-0.75f,0.0,1.0),
Напомню что gl_Position специальная переменная, которую условно можно считать "воз-
вращаемым значением" вершинного шейдера.
После этого GPU проверит, находится ли наш полигон в видимой зоне экрана ( или
буфера ).
Если не видим, то на этом всё для полигона закончится.
Если видим, то занесет его данные в специальный буфер.
Как видите, определить видим полигон или не видим до выполнения кода вершинного
шейдера GPU не может – поэтому не рисуем заведомо невидимые объекты, так как для них
все равно будет отрабатывать вершинный шейдер.
Важно:
Связи между отдельными "потоками просчета" вершинного шейдера нет, так как совре-
менный GPU имеет несколько ядер ( а настольные – уже тысяч ядер ), которые параллельно
обрабатывают свой набор данных, и если бы связь была – пришлось бы делать синхрониза-
цию, что убивало бы все распараллеливание.
В нашем случае:
gl_FragColor = vec4(1.0);
9.2 Varying
Давайте теперь разберем что такое тип varying и для чего они нужны.
То есть значения атрибутов записываются сначала для первой вершины, потом атрибуты
второй и так далее.
Это и есть VAO массив.
Массивы со структурами атрибутов ( VAO ) обрабатываются быстрее, чем набор отдельных
массивов.
И быстрее, чем индексированные массивы.
( про массивы, точнее буферы атрибутов, в следующей части статьи )
void main() {
vColor = aVertColor;
gl_Position = aPosition;
}
[fragment]
void main() {
gl_FragColor = vColor;
}
𝐷 = 𝑑1 + 𝑑2. . . + 𝑑𝑁
𝑤1 = (1 − 𝑑1/𝐷)/(𝑁 − 1)
𝑤2 = (1 − 𝑑2/𝐷)/(𝑁 − 1)
...
𝑤𝑁 = (1 − 𝑑𝑁/𝐷)/(𝑁 − 1)
𝑣𝑇 = 𝑤1 * 𝑣1 + 𝑤2 * 𝑣2. . . + 𝑤𝑁 * 𝑣𝑁
Где:
𝑁 – количество вершин примитива
𝑑1, 𝑑2, 𝑑𝑁 – расстояние между вершиной и точкой T.
𝑣𝑇 – позиция вершины.
Такие как текстурные координаты ( нам же нужны координаты в конкретной точке поли-
гона, а не координаты вершины ), нормали полигонов, цвет вершин и т.д.
9.3 Uniforms
Теперь рассмотрим другой тип переменных – униформы (uniforms).
//Uniform - переменные
float uColor[] = {1.0f, 1.0f, 0.0f} // цвет
float uTrans[] = {0.5f, 0.0f} // сдвиг
void main() {
gl_Position = aPosition;
gl_Position.xy += uTrans;
}
[fragment]
void main() {
gl_FragColor = vec4(uColor,1.0);
}
Код
aPosition.xy += uTrans; // прибавляем к aPosition.xy uTrans.xy
...
// это можно расписать как:
aPosition.x = aPosition.x + uTrans.x;
aPosition.y = aPosition.y + uTrans.y;
Код
gl_FragColor = vec4(uColor,1.0);
// преобразовываем vec3 к vec4 добавлением альфа-канала и задаем цвет.
// в данном случае R=1.0,G=1.0,B=0.0,A=1.0
В общем случае через униформы передаем все значения одинаковые для всего меша,
такие как
матрицы преобразований,
данные о источниках освещения,
данные материала,
время,
трансформации,
в общем всё то, что не меняется от вершины к вершине.
Координаты вершин:
v1(-0.5, -0.5),
v2(-0.5, 0.5),
v3(0.5, 0.5) и
v4(0.5, -0.5)
При обходе вершин против часовой стрелки по умолчанию считается, что полигон на-
правлен лицевой стороной на нас.
9.4.2 Трансформации
Нас интересуют три трансформации:
Translate – параллельный перенос
Scale – масштабирование ( скалирование )
Rotate – поворот
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 83
9.4.3 Translate
Пускай перенос задан двумерным вектором 𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒 (0.25, 0.25)
Для того, чтобы перенести вершины, нам нужно к каждой вершине покомпонентно при-
бавить вектор 𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒
𝑣 ′ = 𝑣 + 𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒;
Где:
𝑣 ′ – новые координаты вершины,
𝑣 – вершина,
𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒 – вектор переноса.
9.4.4 Scale
Для того, чтобы проскалировать, нужно умножить вектор вершины на вектор скалирова-
ния.
Что получилось:
9.4.5 Rotate
Для того чтобы повернуть вершину ( вектор ) нужно:
𝑥′ = 𝑥 * 𝑐𝑜𝑠(𝑡) − 𝑦 * 𝑠𝑖𝑛(𝑡)
𝑦 ′ = 𝑥 * 𝑠𝑖𝑛(𝑡) + 𝑦 * 𝑐𝑜𝑠(𝑡)
Рисунок:
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 85
А что делать если мы хотим повернуть во круг точки отличной от начала координат?
Для этого нам сначала нужно сделать перенос а уже потом поворот.
Важно:
Для поворота, скалирования и переноса меша вокруг свое оси последовательность дей-
ствий такая:
1. поворачиваем
2. cкалируем
3. переносим
//Uniform - переменные
float uRotate = Pi/2.0; // поворот, 90градусов
float uScale[] = {1.0, 1.0} // скалирование, двумерный вектор
float uTranslate[] = {-0.5, 0.0} // перенос, двумерный вектор
void main() {
highp vec2 pos;
pos.x = aPosition.x*cos(uRotate) – aPosition.y*sin(uRotate);
pos.y = aPosition.x*sin(uRotate) + aPosition.y*cos(uRotate);
pos = pos*uScale + uTranslate;
gl_Position = vec4(pos,aPosition.zw);
[fragment]
void main() {
gl_FragColor = vec4(uColor,1.0);
}
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 87
Результат:
Код в Android-программе
private final float[] VertMtx = new float[16]; // матрица преобразований
Matrix.scaleM(VertMtx, 0, x, y, z);
Важно:
преобразовывать матрицу с помощью класса Matrix нужно в обратном порядке.
Для последовательности преобразования поворачиваем −→ cкалируем −→ переносим
нужно
перенести =⇒ скалировать =⇒ повернуть.
Атрибуты вершин:
Координаты вершин, 2 float-а - a_vertex.
Текстурные координаты, 2 float-a - a_texcoord.
Записаны в массиве:
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 89
void main()
{
v_texcoord = a_texcoord;
gl_Position = u_VertMatrix * a_vertex;
}
[fragment]
void main()
{
gl_FragColor = texture2D(t_texture1, v_texcoord);
}
Далее:
Как видите из массива quadv[] координаты вершин от –1 до 1 по каждой оси, что соот-
ветствует всему экрану.
Координаты текстур перевернуты по оси Y так как в OpenGLES на Android эта ось пере-
вернута.
А если использовать 3x3, то ещё потом придётся преобразовать vec3 к vec4 для записи в
gl_Position, на что как раз уйдет лишний цикл.
Текстурный атрибут a_texcoord передаем в фрагментный шейдер через varying vec2 v_texcoord:
v_texcoord = a_texcoord;
Это нужно для того чтобы GPU интерполировал текстурные координаты между верши-
нами и мы на входе фрагментного шейдера получили координаты для конкретной точки.
Далее:
void main()
{
v_texcoord = a_texcoord;
gl_Position = u_VertMatrix * a_vertex;
}
[fragment]
void main()
{
lowp vec4 color1 = texture2D(t_texture1, v_texcoord);
lowp vec4 color2 = texture2D(t_texture2, v_texcoord);
lowp float tMix = texture2D(t_mix1, v_texcoord).r;
gl_FragColor = mix(color1, color2, tMix);
}
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 92
Что получаем:
Строчками
lowp vec4 color1 = texture2D(t_texture1, v_texcoord);
lowp vec4 color2 = texture2D(t_texture2, v_texcoord);
Атрибуты вершин:
Координаты вершины, 2 float-а - a_vertex.
Текстурные координаты, 2 float-a - a_texcoord.
Записаны в массиве:
Униформы:
Целочисленный указатель на текстурный слот – sampler2D s_Texture1.
Матрица преобразования – u_VertMatrix.
10 ЗАГРУЗКА И КОМПИЛЯЦИЯ ШЕЙДЕРОВ 93
void main()
{
v_texcoord = a_texcoord + (u_AnimVector*u_Time);
gl_Position = u_VertMatrix * a_vertex;
}
[fragment]
void main()
{
gl_FragColor = texture2D(t_texture1, v_texcoord);
}
В виде исходного текста для программиста шейдер представляет из себя строку ( или
массив символов в случае Си ).
Для компиляции шейдера, первым делом, нам нужно создать шейдер и получить ссылку
на него:
int shaderID = GLES20.glCreateShader(int shaderType);
10 ЗАГРУЗКА И КОМПИЛЯЦИЯ ШЕЙДЕРОВ 94
Важно:
Вызов GLES20.glCompileShader приводит к загрузке компилятора шейдера ( который сжи-
рает достаточно много памяти и долгий ).
и линкуем программу:
GLES20.glLinkProgram(prog_id);
Важно:
Осталось подчистить за собой хвосты. Так как в памяти у нас еще висит компилятор его
нужно выгрузить:
GLES20.glReleaseShaderCompiler();
Я думаю понятно, что выгружать компилятор нужно после того, как мы скомпилируем
все нужные нам шайдеры, а не после каждого.
Важно:
Один и тот же шейдер может использоваться в нескольких программах.
Для этого его нужно просто прикрепить к нескольким программам и слинковать их.
11 ЗАГРУЗКА СКОМПИЛИРОВАННЫХ ШЕЙДЕРОВ 95
Где:
Buffer binary – буфер с бинарными данными binaryformat – аппаратнозависимый формат,
обычно разный даже для разных моделей GPU одного производителя.
Ищется в документации от производителей.
n – количество шейдеров для загрузки
shaders – массив указателей шейдеров
offset – смещение относительно Buffer binary
binaryformat – вендерозависимый формат ( смотреть у производителя GPU )
Buffer binary – буффер с данными шейдера.
length – размер буфера.
Минусы:
Шейдеры в бинарном формате вендерозависимы и не совместимы между собой, даже
иногда среди разных чипов одного производителя.
Так что придется делать кучу сборок под все возможные конфигурации оборудования.
Важно:
Скомпилированные шейдеры обычно не совместимы ни с чем кроме целевого железа - так
что, если вы не собираетесь делать кеши для всех видов существующего железа, используйте
компиляцию "на лету".
12 Выгрузка шейдеров
Для удаления шейдера нужно вызвать
glDeleteShader(int shaderID)
Важно:
glDeleteShader помечает шейдер на удаление, но шейдер не будет удален, пока он при-
креплен хотя бы к одной программе.
Для удаление шейдера его нужно отвязать от программ или удалить сами программы.
13 УСТАНОВКА ТЕКУЩЕГО ШЕЙДЕРА 96
Важно:
Вызов glUseProgram "тяжелый", старайтесь делать как можно меньше переключений про-
грамм.
Обычно это болезнь старых движков, переключение материалов по каждому чиху, к ко-
торым примотали поддержку OpenGL 3.0+ или OpenGL ES 2.0+ без переписывания рендера
и менеджера рендеринга, и соответственно хорошо, если половина от реально возможной
производительности на выходе.
Когда будете писать свой рендер сортируйте порядок отрисовки в рамках шейдерной
программы и текстур, а не мешей.
То есть привязали шейдер, установили текстуры, закрепили атрибуты ( буферы ) – и
отрисовываем все меши для данного материала – и только потом переключение на новый.
!Все разработчики GPU предоставляют свои SDK, в состав которых входят компиляторы
и профайлеры для шейдеров.
16 СВЯЗЬ ДАННЫХ ПРИЛОЖЕНИЯ И ШЕЙДЕРА 97
Важно:
Имя не может быть структурой или частью вектора или матрицы.
Структуры нужно задавать по членам, например Light.Force, просто Light использовать
нельзя. И нельзя задавать имена типа vector.x или vector[0].
Вызовы "тяжелые" ( так как происходит поиск по имени и настройка таблицы атрибу-
тов ) использовать на этапе загрузки, не использовать в цикле отрисовки, а то я реально
встречал в коде у вполне известного приложения эти вызовы в цикле отрисовки при каждом
переключении шейдера, что давало огромные тормоза.
регистра скорее всего 16 байт3 или 128 бит, что достаточно для хранения 4 float значений.
Одна униформа может занимать как один так и несколько таких регистров.
Например, mat4 займет четыре регистра, а mat3 три регистра ( три ячейки в регистрах
останутся пустыми, если драйвер не дополнит регистры скалярами )
То есть 128 четырехмерных векторов не обязательно значит 512 отдельных скалярных
униформ.
Если мы делаем отдельную скалярную униформу то она все равно займет один регистр
целиком.
Старайтесь упаковывать униформы вручную, если есть подозрения, что регистров может
не хватить, так как был случай, что драйвер не правильно упаковывал значения, ( вернее
разные драйвера по разному ), что приводило к нехватке регистров и соответственно прило-
жение не работало.
Расположение в памяти:
3
Хотя у некоторых GPU регистры бывают и на 32 байта, для высокой точности, но это не принципиально
16 СВЯЗЬ ДАННЫХ ПРИЛОЖЕНИЯ И ШЕЙДЕРА 99
Важно:
При инициализации таблица атрибутов заполняется нулями.
Таблица хранится для каждой шейдерной программы отдельно и сохраняет значения до
уничтожения самой программы.
Важно:
Перед началом передачи Uniform нам нужно установить правильную шейдерную програм-
му (GLES20.glUseProgram() )
Данные в униформы передаются с помощью семейства процедур
GLES20.glUniform{1|2|3|4}{f|i|ui}
void glUniform1f(int location, float v0); // передача float
void glUniform2f( int location, float v0, float v1); // vec2
void glUniform3f( int location, float v0, float v1, float v2); // vec3
void glUniform4f(int location, float v0, float v1, float v2,float v3); // vec4
void glUniform1i(int location, int v0); // int
void glUniform2i(int location,int v0, int v1); // ivec2
void glUniform3i(int location, int v0, int v1, int v2); // ivec3
void glUniform4i(int location, int v0, int v1, int v2, int v3); // ivec4
Где:
location – ссылка на униформ ( полученная GLES20.glGetUniformLocation )
v0,v1,v2,v3 – данные.
Символы i и f в именах функций обозначают тип данных, целые и float соответственно.
Пример:
В шейдере:
. . .
uniform vec3 EyePos;
. . .
Думаю каждую функцию не имеет смысл расписывать отдельно, главное отличие от преды-
дущих – передача массивов.
Пример:
glUniform3fv(int location, int count, float[] v, int offset);
Которая служит для передачи матриц и аналогична функциям передачи массивов, кроме
параметра transpose.
Если параметр transpose установлен в True, то будет передаваться транспонированная
матрица.
Важно:
Запись данных в таблицу униформ происходит в момент вызова glUniform*
Важно:
Функции передачи униформ обязательно должны соответствовать типу данных в шейде-
ре.
То есть нельзя передавать в vec3 с помощью void glUniform3i(), так как это разные типы
данных и результат будет непредсказуемым.
Или vec4 передавать glUniform13fv().
Важно:
sampler-переменные ( указатель на текстурный слот ) в шейдер можно передавать только
через Uniform1i и Uniform1iv, иначе будет ошибка.
Для задачи одного полигона нам потребуется три такие структуры, такой массив будет
выглядеть так:
{Vertex1, Vertex2, Vertex 3}.
Как помните вершинные атрибуты в шейдере могут быть только float, vec2, vec3, vec4,
mat2, mat3 и mat4.
Важно:
В шейдере OpenGL ES 2.0 атрибуты не могут быть целочисленными данными!
Важно:
Данные для каждой вершины ( структуры ) массива должны быть одного типа и одина-
ково расположены.
Некоторые GPU поддерживают для атрибутов HALF_FLOAT, но так как это уже расши-
рение рассматривать его не буду.
Важно:
Если в типом float всё понятно, передали из приложения float и получили в шейдере float
то, что происходит при передаче например byte? Ведь в шейдере атрибуты могут быть только
типом с плавающей запятой?
Что мы получим в B?
Для первой вершины GPU считает байты от 1000 до 1007, преобразует их во внутренний
формат и запишет в первый регистр для последующей обработки шейдером.
Для второй вершины сделает сдвиг начала на размер данных вершины 1000+15, и считает
данные с 10015 по 10022 и преобразует их во внутренний формат и запишет в первый регистр
и так далее.
19 МАССИВЫ В ПАМЯТИ ПРИЛОЖЕНИЯ. VBO 104
Как видите из рисунка, предположим, что в данном массиве пять атрибутов – [vec2, vec2,
float, float, float], так как, например, можно задействовать отдельно первые четыре байта и
получить float атрибут, то есть отношение байтов к какому либо типу условно.
То есть два элемента short при загрузке преобразуются в vec2, скаляры ubyte, byte и
short преобразуются к float, и все значения атрибутов в зависимости от типа передачи могут
быть нормализованы.
Так как массив представляет собой набор данных, мы сами должны отслеживать размер-
ность и порядок данных и задавать правильный тип и смещение для элементов.
Важно:
Для атрибутов автоматической упаковки в отличие от униформ и варингов не происходит!
Только вручную.
Так что старайтесь максимально упаковывать данные.
Важно:
Данные для одного атрибута должны быть одного типа, например, упаковывать два byte
и short в один атрибут нельзя, на выходе получим мусор.
И, естественно, нельзя упаковывать данные из разных массивов.
Данные каждого регистра могут заполняться как из одного, так и из разных массивов (
любых типов ), но нельзя настраивать выборку одних и тех же данных.
Для работы сначала нам нужно загрузить данные в ByteBuffer или преобразовать любой
обычный массив.
В силу специфики Java не удобно работать со смешанными массивами ( с разным типом
элементов ) поэтому, рекомендую по возможности загружать такие массивы из файла уже
предпросчитанными ( то есть не использовать какие либо форматы - а сразу грузить массив
атрибутов ).
Пускай мы хотим сделать массив, который задает прямоугольный спрайт на весь экран.
Где:
ByteOrder.nativeOrder() – задает порядок байтов, в данном случае системный.
Порядок байтов имеет смысл для многобайтовых типов переменных и задает порядок
записи в многобайтовых типах.
Почитать подробнее
Это нужно, чтобы массив располагался одним блоком в памяти, а элементы float[] массива
могут быть разбросаны ( java не регламентирует расположение элементов, ни порядок слов
в переменных ) и самое главное: ByteBuffer - это единственный способ сделать простой
смешанный ( с разными типами данных ) массив.
Единственный параметр задает размер в байтах, а так как float занимает 4 байта нужно
длину массива quadv.length * 4.
Важно:
Обязательно нужно устанавливать позицию массива на 0 ( buffer.position(0) ), так как
позиция при передаче массива считается его началом ( сишной функции передается указа-
тель на начало ).
где:
первый параметр – число генерируемых буферов.
второй – массив ссылок буферов
третий – отступ от начала массива.
где:
Buffer_id – ссылка на буфер
target – тип буфера.
для GLES20 буферы бывают двух типов:
GL_ARRAY_BUFFER и
GL_ELEMENT_ARRAY_BUFFER.
где:
target – тип буфера, GL_ARRAY_BUFFER или GL_ELEMENT_ARRAY_BUFFER,
size – размер в байтах,
20 БУФЕР ВЕРШИННЫХ АТРИБУТОВ В ПАМЯТИ GPU ИЛИ VBO 107
b – буфер данных,
drawHint – способ использования буфера.
drawHint бывает:
STATIC_DRAW – данные определяются раз и используются много раз без изменений.
DYNAMIC_DRAW –данные используются много раз и иногда переопределяются прило-
жением.
STREAM_DRAW – данные используются только несколько раз и часто переопределяются
приложением.
Важно:
Если массив у вас используется только раз – вообще не имеет смысла переносить буфер
в память GPU, только затормозите приложение.
В этом случае используйте массивы в памяти приложения.
При вызове glBufferData для уже существующего массива уничтожает все старые данные
и пересоздает буфер.
где:
count – количество удаляемых буферов
buffers_id – массив ссылок
offset – смещение от начала массива
где:
target – тип буфера, GL_ARRAY_BUFFER или GL_ELEMENT_ARRAY_BUFFER
offset – смещении от начала буфера
size – количество заменяемых байтов
b – буфер с новыми данными
Замечу, что этот вызов "тяжелый", и если данные меняются очень уж часто – лучше
использовать данные в памяти приложения, а не VBO на стороне GPU.
Он задает спрайт состоящий из двух полигонов, для описания которых нам понадобилось
шесть вершин.
Но спрайт прямоугольный, и для его задачи нам, по идее, достаточно четырех вершин,
так как данные у третей – шестой и второй – четвертой вершин совпадают.
Это, как видите, позволяет сильно оптимизировать, как объем данных, так и количество
расчетов. Так как для второго полигона вершины 2 и 4 уже рассчитаны – соответственно
можно рассчитать только вершину 3, а для остальных использовать кэш ( если железо ко-
нечно позволяет ).
23 ПРИВЯЗКА АТРИБУТОВ МАССИВОВ. ПОДКЛЮЧЕНИЕ И ОТКЛЮЧЕНИЕ АТРИБУТОВ109
Но это применимо только тогда, когда совпадает достаточное количество вершин у меша.
То есть, если задавать индексы для отдельностоящих полигонов ( например системы ча-
стиц ), у которых не совпадают вершины, мы наоборот замедлим приложение.
Некоторые GPU поддерживают тип Int, но так как это за рамками стандарта...
где:
prog_id – ссылка на шейдерную программу,
name – имя атрибута в шейдере,
aAtributeHandle – ссылка на атрибут шейдера.
и
glBindAttribLocation(int prog\_id, int index, String name);
где:
index – номер за каким мы хотим закрепить атрибут.
Если вызвать glBindAttribLocation после линковки, то это может привести к ошибке или
неправильной работе.
То же самое в случае с вызовом glGetAttribLocation до линковки.
После того, как мы получили ссылку ( индекс ) атрибута, нам нужно закрепить за атри-
бутом данные c помощью функции glVertexAttribPointer.
где:
AtributeHandle – ссылка на атрибут,
Size – количество ЭЛЕМЕНТОВ атрибута,
Type – тип данных атрибута для передачи ( а не тип атрибутов в шейдере).
Может принимать значения:
GL_BYTE,
GL_UNSIGNED_BYTE,
GL_SHORT,
GL_UNSIGNED_SHORT,
GL_FIXED и
GL_FLOAT.
Обращаю внимание, что от системы к системе значения могут меняться, то есть такая
размерность не гарантируется.
Для правильной работы должен быть подключен атрибут после закрепления текущего
шейдера (glUseProgram).
После того, как закончили работу ( отрисовку ) текущего массива, атрибут нужно от-
ключить:
glDisableVertexAttribArray(aHandle);
Важно:
отключение атрибутов обязательно для массивов в памяти GPU, но рекомендую отклю-
чать и для локальных массивов.
Локальный массив:
пускай у нас массив состоит из двух атрибутов, Position – float[2] и Texture – float[2].
mTriangleVertices – массив атрибутов.
private FloatBuffer mTriangleVertices;
....
23 ПРИВЯЗКА АТРИБУТОВ МАССИВОВ. ПОДКЛЮЧЕНИЕ И ОТКЛЮЧЕНИЕ АТРИБУТОВ112
mTriangleVertices.position(0);
GLES20.glVertexAttribPointer(aPositionHandle, 2, GLES20.GL_FLOAT, false, 16,
mTriangleVertices); // подключаем первый атрибут
GLES20.glEnableVertexAttribArray(aPositionHandle);
mTriangleVertices.position(2);
GLES20.glVertexAttribPointer(aTextureHandle, 2, GLES20.GL_FLOAT, false, 16,
mTriangleVertices); // подключаем второй атрибут
GLES20.glEnableVertexAttribArray(aTextureHandle);
...
[ отрисовка примитивов для данного массива ]
...
GLES20.glDisableVertexAttribArray(aPositionHandle); // для локальных массивов можно
не отключать, но все равно настоятельно рекомендую отключать вручную,
GLES20.glDisableVertexAttribArray(aTextureHandle); //привыкая убирать хвосты.
Важно:
Для массивов в памяти GPU отключение ОБЯЗАТЕЛЬНО.
Важно:
Атрибуты можно подключать смешано с разных типов массивов ( локальных или в памяти
GPU).
То есть часть атрибутов может быть в памяти GPU, часть в памяти приложения и при-
надлежать разным массивам.
24 "Заглушки"атрибутов
Может возникнуть ситуация, когда требуется выставить атрибут в определенное ( един-
ственное ) значение атрибута для всего меша, так как не гарантируются значения для непод-
ключенных атрибутов, и при этом не имеет смысла расходовать память для указания одних
и тех-же значений.
где:
index - ссылка на атрибут,
values - значения атрибута.
25 ПОЛУЧЕНИЕ ИНФОРМАЦИИ О ПОДКЛЮЧЕННЫХ ШЕЙДЕРАХ, УНИФОРМАХ, АТРИБУТАХ1
public static void glGetActiveUniform (int program, int index, int bufsize,
IntBuffer length, IntBuffer size, IntBuffer type, byte name)
public static void glGetShaderSource (int shader, int bufsize, IntBuffer length,
byte source)
Детально разбирать их не буду, но общий смысл в том, что можно получить текущее
значение и/или атрибута/униформы/исходник_шейдера.
Важно:
Некоторые компиляторы шейдеров не выдают ошибку при записи значения в атрибут, но
это не значит, что значения атрибута при записи из шейдера изменятся!
То есть получить измененное значение униформы ( атрибута ) нельзя, так как на самом
деле шейдер не может менять эти значения.
ОБРАТНОЙ СВЯЗИ с шейдером НЕТ.
Важно:
В андроид сломан враппер на данных функциях, если они нужны, то следует использо-
вать внешний враппер.
26 Отрисовка примитивов
Для отрисовки примитивов используется несколько функций в зависимости от типа мас-
сива ( массив индексов или "простой").
где:
mode – тип примитивов для отрисовки.
Бывает:
GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES,
GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES.
first – первая вершина
count – количество вершин для отрисовки
Пример:
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6); // отрисует шесть вершин и соответс
твенно два полигона
где:
mode – тип примитивов для отрисовки.
Бывает:
GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES,
GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES.
Вот и всё.
Спрайт – сейчас под этим понимается несколько достаточно разных значений. В пер-
воначальном смысле "спрайт" – это небольшое изображение, которое мы свободно можем
перемещать по экрану.
В данный момент то, что понимается под спрайтом, можно разделить на несколько групп:
Decal ( декаль ) – в общем смысле средство для повышения детализации, обычно накла-
дывается поверх основной геометрии. Из примеров – граффити на стене, "дырка" в обоях,
плакат и т.д. Грамотное использование декелей позволяет увеличить детализацию и краси-
вость картинки в несколько раз при совсем небольших затратах.
Обычно "новички" пренебрегают декалями – из-аз чего их работы выглядят достаточно
печально. . .
Стандартная ошибка – например стена в повторяющейся текстурой обоев размноженное
много раз по всем осям.
Такая стена будет выглядеть печально и фальшиво.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 117
А вот если добавить пару пятен – потертостей, небольшое "оттенение" некоторых участ-
ков, "повесить" даже маленькую картину – то будет совершенно другой вид.
27.1 Билборды
Под билбордом обычно понимают прямоугольник перпендикулярный камере ( взгляду ) –
лицевой стороной ориентированный на камеру.
Хотя возможны для некоторых задач варианты, когда требуется определенный угол по
отношению к камере.
Достоинства Билбордов:
1. C ними можно делать всё что можно делать с "обычной" геометрией.
Недостатки:
1. Большая избыточность данных. Требует четыре вершины для задания каждого бил-
борда, ещё обычно добавляют центр билборда, размер.
2. Сильная нагрузка на вершинные шейдеры.
3. Большая нагрузка на CPU и шину, как следствие задачи большого числа данных.
4. Проблема множественной перерисовки при маленьком размере частиц.
Посмотрим на рисунок:
На самом деле, GPU будет просчитывать эти пикселя ДВА раза для каждого полигона
и смешивать в итоговое значение. Без этого швы были бы слишком сильно заметны и "на
глаз" модели бы распадались на полигоны.
Вообще это общая проблема всей геометрии – и представьте, что происходит, когда
моделька в несколько тысяч полигонов уменьшается до нескольких пикселов. . .
На экране в итоге мы видим несколько десятков пикселей – а просчитаны были тысячи.
Большинство мобильных GPU подвержены такому неприятному эффекту.
Спасает только LOD – да и то не всегда.
Например в данном случае лишняя отрисовка при удалении от "столба" будет процентов
как минимум 70% из-за наличия большого количества "узких" полигонов.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 120
Для маленьких спрайтов всего этого, как и других проблем билбордов, можно избежать
задействовав точечные спрайты.
Точечные спрайты ( Point Sprites ) – или как иногда называют – микрополигоны являют-
ся одним из примитивов которые может рендерить GPU на OpenGL.
Размер точки задается в пикселах – т.е. в конечном размере, как она будет выглядеть на
экране, поэтому, чтобы получить одну и туже картинку на разных устройствах, нужно это
учитывать.
Важно:
Точечный спрайт ВСЕГДА квадратный и ВСЕГДА расположен параллельно осям экрана
– мы НЕ МОЖЕМ поворачивать микрополигоны.
где:
первый параметр будет отвечать за минимальный размер,
второй – за максимальный.
Размеры в пикселах.
Для точки мы не передаём текстурные координаты – так как задается она одной точкой
– но в фрагментном шейдере есть специальные входящие параметры с текстурными коорди-
натами, которые генерируются автоматически.
Вершиный шейдер:
gl_PointSize – исходящий параметр, задает размер РАСТАРИЗОВАННОЙ точки.
Фрагментный шейдер:
gl_PointCoord – сгенерированные текстурные координаты для спрайта.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 121
void main()
{
vColor = vec4(aColor,1.0);
gl_PointSize = aSize;
[fragment]
void main()
{
gl_FragColor = vColor*texture2D(t_texture, gl_PointCoord);
}
Этот шейдер может быть использован для построения ПРОЦЕДУРНОЙ системы частиц.
т.е. раз задав значения и меняя только один или несколько параметров, приводим всю
систему в движение.
экрана.
Для исправления этого достаточно чуть увеличить "рамки"
– например mod(. . ., 2.1) – 1.05
Всё вместе:
"gl_Position.xy = mod((aVector*aSpeed*uTime + aPositionStart),2.0) – 1.0;"
Атрибуты из данного примера в реальной задачи лучше упаковать в два атрибута – так
как использовать отдельно несколько float-атрибутов и vec2 нерационально.
Главное достоинство такой системы частиц – то, что мы задаем начальные данные только
на этапе инициализации – а дальше
спокойно можем передать их в память GPU и анимировать всю систему на несколько
тысяч частиц
передачей нескольких параметров через униформы – в данном случае время.
Можно добавить вектор ветра и любые другие значения, которые будут влиять на систе-
му и направление движения частиц.
Давайте рассмотрим другой вариант построения системы частиц – дым из трубы парово-
за ( или от спички ) которая двигается.
В этом случае мы не знаем заранее где эмиттер ( излучатель частиц, место где частицы
появляются ) – так что все просчитать заранее не получится.
Но всё равно у нас есть куда данных которые можно обработать процедурно.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 123
Теперь представим, что будет, если все параметры каждой частицы мы будем обновлять
каждый кадр на CPU...
А если таких систем еще несколько?
Слайд-шоу. Что и было бы на ранних версиях OpenGL.
На самом деле, каждый кадр достаточно будет обновить параметры только ТРЕХ частиц
– а остальные спокойно посчитаются процедурно.
Первым делом генерируем массив на 1260 спрайтов и зададим только позицию выходя-
щую далеко за экран.
Далее каждый кадр ( или через один например =) ) будем обновлять атрибуты трех
частиц, т.е. задавать:
Начальную позицию, вектор дальнейшего движения, размер, продолжительность жизни
и т.д.
main(){
. . .
If(aTimeOfDeath<=time) {
gl_Position = vec(1000.0,1000.0,1.0,1.0); // отправляем спрайт к черту на куличи
ки
}else{
// спрайт жив. Рассчитываем все как обычно.
. . .
}
. . .
}
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 124
Когда "дата смерти" спрайта будет меньше текущей даты – мы точно уверены, что спрайт
мертв – и отправляем его по координатам далеко за экран.
По таким координатам он не будет растерезовыватся на 100%.
Соответственно таким способом отсеиваются мертвые спрайты – причем очень "дешево"
в плане вычислений.
Все данные одного спрайта мы рассчитываем/задаем только ОДИН раз – дальше все
считаем процедурно.
Получается хорошая экономия мощностей.
Когда мы дойдём до конца массива – 1260 позиции, мы уже будем уверены, что спрайты
под первыми позициями "умерли" и переносим указатель на начало массива.
п.с. никто не мешает отрисовывать хоть десять источников из одного массива – тогда
таким способом ещё можно регулировать количество спрайтов одновременно на экране –
при нехватке "свободных" будут затираться самые старые,
что в глаза не сильно будет бросятся – но будет лучше в плане стабильности fps.
А если попытаться считать всё это по старинке – ничего кроме тормозов не получим.
И даже если мощностей CPU хватит GPU все равно будет справляться с этой задачей
эффективней хотя бы в плане затрат аккумулятора – а уж поверьте, пользователям есть
разница посадит игра/приложение телефон за полтора часа или за пять.
void main()
{
vec2 Position;
if(uTime>=aLifeTime.y){
Position = vec2(1000.0,1000.0); // отправляем спрайт "за экран"
}else{
float delta = 1.0 - (aLifeTime.y-uTime)/aLifeTime.x;
// вычисляем возраст жизни частицы, от 0.0 до 1.0, 1.0-смерть
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 125
[fragment]
В проекте используется частичная замена данных в буфере атрибутов GPU и из-за про-
блем с буферами ( ошибки ) в версиях Android меньше чем 2.3 минимальная версия API
9.
Для того, чтобы запустить на 2.2 нужен внешний ( или исправленный ) парсер OpenGL.
Достоинства:
1. Нет проблемы “овердроу” при маленьком размере спрайта почти на всех GPU.
2. Требуется только обработка одной вершины и не требует дополнительных операций
для поворота к "зрителю", что создает в несколько раз (!) меньшую нагрузку на вершинные
блоки GPU.
3. Позволяет просто создавать большие динамические системы без кучи избыточных
данных – что позволяет экономить память и как самое главное – пропускную шины данных.
4. На большинстве GPU быстрее растеризуются чем обычные полигоны.
5. текстурные координаты генерируются автоматически. Нет необходимости в дополни-
тельных атрибутах и обработках.
Недостатки:
1. Имеют ограниченный размер. Хотя как минимум 64 пикселя.
2. Всегда квадратны. Никаких прямоугольников и т.д.
3. Грани спрайта ВСЕГДА параллельны сторонам экрана – т.е. невозможно повернуть
частицу.
4. Размер спрайта привязан к пикселям экрана.
5. текстурные координаты генерируются автоматически ( пункт не ошибка =) ) что не
подходит для некоторых задач.
Например, нам нужно выбрать спрайт из атласа 5x5 ( в атласе спрайты распределены
равномерно ), пускай координаты спрайта [1,2].
Внимание:
Давайте обратим внимание на строчку gl_PointCoord*vAtlasRes+SpriteTr;
Пример шейдера
[vertex]
void main()
{
float absTime = fract(aDeltaPos+uTime);
vColor = mix(uColorStart,uColorEnd,absTime);
vAtlasRes = uAtlasRes; // передаем обратное размеру атласа
vSprite = uAtlasRes*aSprite; // вычисляем смещение
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 128
[fragment]
precision mediump float;
void main()
{
gl_FragColor = texture2D(t_texture, (gl_PointCoord*vAtlasRes+vSprite))*vColor;
}
Например, для поворота вокруг центра текстуры нам нужно перенести координаты сере-
дины ( [0.5,0.5] ) в нулевую точку – затем повернуть – и перенести обратно.
Таким трансформациям соответствует следующая матрица
float CosA = cos(angle);
float SinA = sin(angle);
Я думаю, понятно, что вычислять такую матрицу нужно в вершинном шейдере и переда-
вать в фрагментный уже готовой.
Умножение на матрицу занимает как максимум 3 такта\цикла, хотя некоторые GPU мо-
гут делать такие операции и за один цикл.
Так как все трансформации приходится вычислять в фрагментном шейдере, понятно, что
они будут выполнятся для каждого пикселя спрайта – что при большом размере спрайта
перевесит недостатки обычных билбордов в лишних операциях.
(исходников нет)
Если спрайты достаточно большие, больше нескольких десятков единиц по каждой оси и
требуют сложных трансформаций или плохо вписываются в квадрат, то лучше использовать
обычные билборды ( полигоны ) – так как все плюсы использования точечных спрайтов
пропадут в этом случае.
Например, понятно, что длинный и тонкий объект не очень хорошо вписывать в квадрат,
и, используя в данном случае ( рис. ) квадратные микрополигоны, мы потеряем кучу ресур-
сов впустую:
Половина чипов несимметричны ( Tegra, Mali ) и число блоков в них обычно от 1к4 до
1к2 - вершиных к фрагиментным.
И вполне может возникнуть ситуация, когда вершинные блоки перегружены – а фраг-
ментные простаивают.
Поэтому выбирать какие блоки лучше и как нагружать нужно под конкретную задачу.
Хотя никто не мешает нам использовать смешанную систему – точечные спрайты впере-
мешку с билбордами используя сильные стороны обоих подходов.
Многим кажется ( почему-то ), что прозрачные точки совсем ничего не весят – хотя на
самом деле они тяжелее в вычислении чем непрозрачные пикселя.
( текст еще буду вычищать, выложил как есть пока - а то это может длится долго. )
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 131
Верстальщик