Вы находитесь на странице: 1из 131

Содержание

1 Описание OpenGL 5

2 Пример простейшей программы с OpenGL 9

3 EGL. Инициализация OpenGL ES 11


3.1 Как устроен GLSurfaceView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
3.2 Конфигурации GLSurface . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3.3 Про антиалайзинг ( сглаживание ) . . . . . . . . . . . . . . . . . . . . . . . . . . 19

4 Методы GLSurfaceView 19
4.1 Методы GLSurfaceView . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4.2 Реализация интерфейса EGLConfigChooser . . . . . . . . . . . . . . . . . . . . . 21
4.3 Реализация Renderer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22
4.4 Стабилизация FPS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
4.5 Реализация Activity . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

5 Отступление А: Движки, Оптимизация... 26


5.1 Лучшее – враг хорошего или будьте проще . . . . . . . . . . . . . . . . . . . . . 26
5.2 Любителям ООП . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5.3 Запас прочности и производительности . . . . . . . . . . . . . . . . . . . . . . . 28
5.4 Ненужный код . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
5.5 Движки или что под этом понимается . . . . . . . . . . . . . . . . . . . . . . . 29

6 Отступление B: Примитивы и прочее 30


6.1 Примитивы OpenGL ES 2.0 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
6.1.1 ПРИМИТИВ Triangle (Треугольник) . . . . . . . . . . . . . . . . . . . . 31
6.1.2 ПРИМИТИВ Line (Линия) . . . . . . . . . . . . . . . . . . . . . . . . . . 33
6.1.3 ПРИМИТИВ Point Sprites (Точечные спрайты) . . . . . . . . . . . . . . 33
6.2 Матрицы, вектора. Преобразования в OpenGL . . . . . . . . . . . . . . . . . . . 34
6.2.1 Матрицы . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
6.2.2 Преобразования . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
6.2.3 Система координат в Android . . . . . . . . . . . . . . . . . . . . . . . . . 36

7 Текстуры в OpenGL ES 2.0 37


7.1 NPOT и POT текстуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
7.2 Память в Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
7.3 Максимальный размер текстуры . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
7.4 Текстурные координаты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
7.5 Текстурные слоты (Texture Units) . . . . . . . . . . . . . . . . . . . . . . . . . . 43
7.6 Семплирование текстур . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
7.7 Хранение текстур в памяти . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
7.8 Загрузка текстуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
7.9 Удаление текстур . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
7.10 Сжатие текстур . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
7.10.1 Система расширений . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
7.10.2 Загрузка сжатых текстур . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
СОДЕРЖАНИЕ 2

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

9 Примеры шейдеров. Разбор работы 72


9.1 Пример простейшего шейдера . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
9.2 Varying . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
9.3 Uniforms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
9.4 Трансформации примитивов в 2D . . . . . . . . . . . . . . . . . . . . . . . . . . 81
9.4.1 Связь с координатами Android . . . . . . . . . . . . . . . . . . . . . . . . 81
9.4.2 Трансформации . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
9.4.3 Translate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
9.4.4 Scale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
9.4.5 Rotate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
9.4.6 Пример. Простая трансформация в 2D . . . . . . . . . . . . . . . . . . . 86
9.5 Пример. Текстуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
9.6 Пример. Несколько текстур. Смешивание . . . . . . . . . . . . . . . . . . . . . . 91
9.7 Пример. Анимация текстуры . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92

10 Загрузка и компиляция шейдеров 93

11 Загрузка скомпилированных шейдеров 95

12 Выгрузка шейдеров 95
СОДЕРЖАНИЕ 3

13 Установка текущего шейдера 96

14 Проверка правильности ( валидация ) шейдерной программы 96

15 Общие рекомендации по загрузке шейдеров 96

16 Связь данных приложения и шейдера 97


16.1 Получение указателей на атрибуты и униформы . . . . . . . . . . . . . . . . . . 97
16.2 Передача данных в шейдерную программу . . . . . . . . . . . . . . . . . . . . . 97
16.2.1 Передача униформ из приложения в шейдер . . . . . . . . . . . . . . . . 97
16.2.2 Общие рекомендации по униформам . . . . . . . . . . . . . . . . . . . . 100

17 Буферы атрибутов вершин. VBO 101

18 Таблица ссылок атрибутов и таблица атрибутов 103

19 Массивы в памяти приложения. VBO 104

20 Буфер вершинных атрибутов в памяти GPU или VBO 106

21 Изменение части данных буфера в памяти GPU 108

22 Буферы индексов вершин 108

23 Привязка атрибутов массивов. Подключение и отключение атрибутов 109

24 "Заглушки"атрибутов 113

25 Получение информации о подключенных шейдерах, униформах, атрибутах 114

26 Отрисовка примитивов 114

27 Системы частиц. Билборды. Точечные спрайты 116


27.1 Билборды . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 117
27.2 Точечные спрайты . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
Для чего эта тема?

У многих создалась иллюзия сложности изучения "OpenGL и не понимания простоты ра-


боты этой библиотеки для программиста. И даже используя "движок"нужно понимать как
это взаимодействует с ОС, что может/не может конкретные устройства.

В данной статье постараюсь выйти за рамки стандартных примеров – а именно постара-


юсь рассказать почему и за чем. ( тем более я давно это пообещал ) От читателей требуется
хотя бы поверхностное знание любого ЯП.

Исправления приветствуются.
Все дальнейшее посвящено библиотеке OpenGL ES 2.0 под Android, и последующим версиям.
1 ОПИСАНИЕ OPENGL 5

1 Описание OpenGL
Что такое библиотека OpenGL ES 2.0?

На базовом уровне, OpenGL ES 2.0 — это просто спецификация, то есть документ, опи-
сывающий набор функций и их точное поведение. Производители оборудования на основе
этой спецификации создают реализации — библиотеки функций, соответствующих набору
функций спецификации ( W: ).

OpenGL ориентируется на следующие две задачи: Скрыть сложности адаптации различ-


ных 3D-ускорителей, и предоставить разработчику единый API.

Для программиста OpenGL представляет низкоуровневую библиотеку для доступа к GPU


(графическому процессору).

Схема вариантов реализации библиотеки ( с точки зрения программиста + для сравнения


DirectX ):

В Android на 99.99% используется вариант В.

То есть реализация OpenGL ES входит в состав драйвера, в отличие от DirectX,


которая скорее является прослойкой между приложением и драйвером.
Есть еще отдельные реализации OpenGL, например Mesa3D, но они в основном доста-
точно медленно развиваются и часто отстают на несколько поколений от решений произво-
дителей чипов.
1 ОПИСАНИЕ OPENGL 6

Что лучше, DirectX или OpenGL?

Вопрос не корректный.

Например если нужна мультиплатформенность — про DirectX можно забыть. И на взгляд


автора DirectX слишком «оброс» хвостами... ( но это очень субъективно ) + Сравнивать не
совсем корректно, так как DirectX кроме графики реализует много интерфейсов ( и вполне
кошерных – включая звук, ввод, сеть и т. д. )

Что быстрее, DirectX или OpenGL?

Тоже не корректный вопрос, все зависит от опытности программиста.

Но опять, на взгляд автора ( меня =) ), нестандартные возможности проще реализовы-


вать на современных версиях OpenGL, и, тем более, для этого не требуются переходы на
новые операционные системы ( в отличие от DirectX 10 ).

Времени на изучение тоже требуется на порядок меньше. + переносимость.

Теперь чуть-чуть про GPU:

В данный момент (декабрь 2012г.) в Android устройствах присутствуют два поколения


GPU, поддерживающие OpenGL ES 2.0 ( почти 95% ) и поддерживающие только версии 1.0
и 1.1.
Аппаратной обратной совместимости НЕТ.

Поэтому рассматривать версию стандарта меньше 2.0 на взгляд автора кроме как архео-
логам не имеет смысла. (стандарт версии 3.0 обратно совместим с 2.0)
1 ОПИСАНИЕ OPENGL 7

Структура конвейера OpenGL 1.x:

Структура конвейера OpenGL 2.x+:


1 ОПИСАНИЕ OPENGL 8

То есть часть блоков с «железной логикой» заменили на программируемые процес-


соры.
Вопрос: а для чего?
А все дело в том что аппаратно было реализовано достаточно мало функций, из-за этого со-
здавались существенные ограничения в дальнейшем развитии и гибкость была равна нулю.

История ( немного ):

Первые попытки перенести расчеты с CPU ( центрального процессора ) были реализованы


в первом Geforce ( а не в Voodoo, как думают многие ), называлать технология T&L.
Она позволяла аппаратно просчитывать на GPU освещение и выполнять уже простейшие
шейдеры.
Получилось «быстро», но не осталось даже минимальной гибкости. Есть аппаратно-
реализованный метод освещения, например, – используем. Нет — и не будет.
Следующая веха – GeForce 3, который обладал уже вполне программируемой логикой, но
процессорные блоки еще небыли универсальными.
То есть блоки делились на обрабатывающие вершины и фрагментные ( обрабатывающие
пиксели ).
Одни могли быть перегружены, другие простаивали...
В чем смысл наращивания процессоров ( вычислительных блоков ) у GPU?
Все дело в том что графические просчеты почти линейно маштабируется, то есть увеличение
процессоров например со 100 до 200 дает почти 100% прирост производительности, так как
в компьютерной графике текущий расчет обычно не зависит от предыдущего — то есть легко
запаралелить.
Но и существуют некоторые ограничения, о которых будет написано ниже.

Теперь про сам OpenGL ES:

Что может OpenGL ES?

Основным принципом работы OpenGL является получение наборов векторных графиче-


ских примитивов в виде точек, линий и многоугольников с последующей математической
обработкой полученных данных и построением растровой картинки на экране и/или в памя-
ти.
Векторные трансформации и растеризация выполняются графическим конвейером (graphics
pipeline), который по сути представляет собой дискретный автомат.
Абсолютное большинство команд OpenGL попадают в одну из двух групп: либо они
добавляют графические примитивы на вход в конвейер, либо конфигурируют конвейер на
различное исполнение трансформаций.
Ключевая особенность — CPU и GPU работают не синхронно, то есть CPU не дожидается
окончания исполнения команд от GPU, а продолжает работать ( если не было дополнитель-
ных указаний ).
Есть стек1 команд (инструкций) OpenGL.
В OpenGL используется FIFO ( очередь ).

1
Cтек бывает двух типов, FIFO и LIFO. FIFO — акроним «First In, First Out» (англ.). Принцип «первым
пришёл — первым ушёл», LIFO — акроним «Last In, First Out» (англ.), обозначающий принцип «последним
пришёл — первым ушёл».
2 ПРИМЕР ПРОСТЕЙШЕЙ ПРОГРАММЫ С OPENGL 9

2 Пример простейшей программы с OpenGL


Теперь пример простейшей инициализации OpenGL ES 2.0 под Android ( из примеров
Google, абсолютно корявый и не применимый в реальной жизни =) ):
В приложении, методе OnCreate:

Листинг 1: метод OnCreate


super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE); // Убираем заголовок
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN); // Устанавливаем полноэкранный режим
try { // пытаемся инициализировать OpenGL
glSurfaceView = new GLSurfaceView(this);

// Далее устанавливаем версию OpenGL ES, равную 2


glSurfaceView.setEGLContextClientVersion(2);

renderer = new nRender();


glSurfaceView.setRenderer(renderer); // устанавливаем нашу реализацию
GLSurfaceView.Renderer для обработки событий

glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
// режим смены кадров
// RENDERMODE_CONTINUOUSLY - автоматическая смена кадров,
// RENDERMODE_WHEN_DIRTY - по требованию ( glSurfaceView.requestRender(); )

setContentView(glSurfaceView);
} catch (RuntimeException e) {} // выводим окошко "увы, выше устройство слишком..."

Далее создаем класс nRender


2 ПРИМЕР ПРОСТЕЙШЕЙ ПРОГРАММЫ С OPENGL 10

Листинг 2: класс nRender


package com.example.ogl1;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import java.util.Random;

public class nRender implements GLSurfaceView.Renderer


{

public nRender() { }

public void onDrawFrame(GL10 glUnused) {


// Отрисовка кадра

Random rnd = new Random();


// Задаем случайный цвет и сводим с ума эпилептиков =)
// Цвет задается в формате RGBA, от 0.0f до 1.0f.
GLES20.glClearColor(
((float)rnd.nextInt(2)/2.0f),
((float)rnd.nextInt(2)/2.0f),
((float)rnd.nextInt(2)/2.0f),
1.0f
);

GLES20.glClear( GLES20.GL_COLOR_BUFFER_BIT ); // Очищаем буффер цвета


}

public void onSurfaceChanged (GL10 glUnused, int width, int height) {


// изменение поверхности, например изменение размера

GLES20.glViewport(0, 0, width, height);


// Устанавливаем положение и размер вьюпорта
// вьюпорт устанавливаеться относительно поверхности ( OpenGLSurface ),
//в данном случае на весь экран.
// замечу, что GLES20.glClear очищает всю поверхность,
//все зависимости от установки Viewport.
}

public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {


// вызываеться при создании поверхности
}
}

Далее пробуем запустить, и получаем мечту эпилептика...


Данный пример каноничен ( по документации ), но убог и уродлив по всем проявлениям....
3 EGL. ИНИЦИАЛИЗАЦИЯ OPENGL ES 11

OpenGL по своему принципу является конечным автоматом.

Что это значит?


Представьте себе конвейер производства дед.мороза =)
С одной стороны вы забрасываете заготовки, с другой стороны выходит готовая продук-
ция.
Но вы стоите за пультом, у которого есть несколько рычагов – и, в зависимости от переклю-
чения Вами этих рычагов, выходят танки, куклы, хлопушки.
Но никогда не может появиться на свет кукла-хлопушка, то есть в данный момент времени
возможен только один вид продукции.
Линия находится всегда только в одном состоянии, и может в данный момент времени вы-
пускать только определенную продукцию.
Это и есть машина конечных состояний. Проще объяснить не могу. Кто не понял – тут
Продолжение как чуть высплюсь... ( следующая тема – что скрывается за GLSurfaceView,
чем это плохо и что такое EGL. )

3 Что скрывается за GLSurfaceView или библиотека EGL.


Подробный разбор инициализации OpenGL ES.
За GLSurfaceView скрывается инициализация OpenGL с помощью библиотеки EGL.
EGL – интерфейс между API графического адаптера, таких как OpenGL ES и OpenVG и
системой управления окнами платформы.
EGL также обеспечивает возможность взаимодействия между API для эффективной пере-
дачи данных – например между видеоподсистемой работающей c OpenMAX AL и GPU
работающем с OpenGL ES.
EGL предоставляет механизмы для создания поверхности ( Surface ), на которой клиент
API, такой как OpenGL ES или OpenVG создает графику.
EGL синхронизирует клиент API и родной API визуализации для платформы ( в случае
Android это Skia).
Это позволяет бесшовную, высокопроизводительную , ускоренную визуализацию с использо-
ванием как OpenGL ES так и OpenVG для смешанного режима 2D и 3D-рендеринга.
Cистема управления окнами ( native platform window system ) – оконная система, обеспе-
чивающая стандартные инструменты и протоколы для построения графического интерфейса
пользователя.
В случае Android это SurfaceFlinger, для *nix это обычно X Window System, MacOs и
Windows используют свою, ни с чем не совместимую систему.
В ранних реализациях стандарта OpenGL был пропуск в этапе создания GLSurface по-
верхности в контексте оконной системы.
Для инициализации приходилось использовать средства самой операционной системы, ко-
торые сильно отличались друг от друга и даже не всегда была возможность обеспечить
одинаковый вид и функциональность
Нестандартных реализаций было достаточно много — это SDL, GLUT,CPW,NGL и т.д.
Но все же единого стандарта не было.
Библиотека EGL создана для того чтобы закрыть этот пробел.
OpenVG – стандартный API, предназначенный для аппаратно-ускоряемой двухмерной
векторной графики.
Так как на платформе Android этот API не задействован ( даже странно, почему так )
3 EGL. ИНИЦИАЛИЗАЦИЯ OPENGL ES 12

рассматривать его не буду. =(

3.1 Как устроен GLSurfaceView. Процесс инициализации


Посмотрим, что же делает GLSurfaceView( исходник ):
( Выделяю только ключевые моменты )
public class GLSurfaceView extends SurfaceView

Из этой строчки видно что GLSurfaceView является расширением SurfaceView.


SurfaceView предоставляет выделенные поверхности для рисования, встроеную в иерар-
хию Activity.
Вы можете управлять форматом этой поверхности и, если хотите, ее размер;
SurfaceView заботится о размещении поверхности в нужное место на экране.
Соответственно можно размещать view-шки как на обычном SurfaceView или например
встроить GLSurfaceView в свой элемент управления.
...
holder.setFormat(PixelFormat.RGB_565);
...

Далее GLSurfaceView задает формат своей поверхности как RGB_565 ( 16bit цвет ).
На это стоит обратить внимание, подробности чуть ниже.
Теперь сама инициализация OpenGL:
...
EGL10 Egl = (EGL10) EGLContext.getEGL();
...

Получение экземпляра враппера EGL.


Wrapper ( враппер ) – обёртка библиотеки, является промежуточным слоем между при-
кладной программой и другой библиотекой или интерфейсом программирования приложений
(API).
Целью написания обёртки библиотеки может быть обеспечение работоспособности биб-
лиотеки (API) в каком-либо (чаще скриптовым) языке, в котором прямой вызов функций
этой библиотеки API затруднителен или невозможен.
В Android классы GLES и EGL являются обертками к нативным библиотекам OpenGL и
EGL.
...
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
...

Получение ссылки на стандартный дисплей.


Дисплеев в системе может быть несколько, если нужен доступ к другим дисплеем кон-
станту EGL_DEFAULT_DISPLAY нужно заменить на соответствующею.
Все константы кроме EGL_DEFAULT_DISPLAY платформозависимы.
...
int[] version = new int[2];
mEgl.eglInitialize(mEglDisplay, version);
...

Инициализация EGL для конкретного дисплея. Возвращает версию EGL в массиве version[].
...
EGLConfig mEglConfig = mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);
...
3 EGL. ИНИЦИАЛИЗАЦИЯ OPENGL ES 13

Выбирается конфигурация OpenGL поверхности.


Это самый важный момент в инициализации OpenGL.
Именно на этом этапе можно задать глубину цвета, буферов, антиалайзинг и еще кучу
параметров.
Ниже будет очень подробное описание.
...
mEglContext = egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT,
mEGLContextClientVersion != 0 ? attrib_list : null);
...

Создаем контекст определенной конфигурации.


Очень "тяжелая"операция.
Именно в этот момент начинает работать часть графического драйвера отвечающего за 3D,
происходит куча проверок, инициализируются большие объемы памяти и т.д.
Все, OpenGL инициализирован.
Теперь создание GLSurface:
...
egl.eglCreateWindowSurface(display, config, nativeWindow, null);
...

Cоздается одним вызовом. nativeWindow – ссылка на поверхность/окно, где создастся


OpenGLSurface.
( прошу обратить внимание на то что этот метод можно вызвать только из SurfaceView или
его наследников из-за ограничения во враппере EGL )
И устанавливает текущей контекст:
...
mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext);
...

Вот и все. Можно рисовать.


OpenGL контекст создается за 7 вызовов EGL.

Все выше приводилось для понимания процесса инициализации.


Не буду приводить полный рабочий пример инициализации, так как заниматься изобре-
тением велосипедов не имеет смысла и основа приложения в дальнейших примерах будет
GLSurfaceView, с небольшими изменениями.
Ещё одна необходимая функция EGL – смена кадров ( свап буферов ):
...
mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)
...

Тут я думаю все понятно.

3.2 Конфигурации GLSurface


Теперь разберемся с конфигурациями GLSurface.
OpenGLSurface конфигурации – это возможность создания OpenGLSurface с разными
параметрами, такими как глубина цвета, сглаживание, глубина Z-буфера/трафорета и еще
кучей параметров.
Для программиста шаблон конфигурации представляет из себя одномерный массив целых
чисел с парами ключ-значение ( записанными последовательно ).
Массив всегда должен заканчивается ключом EGL10.EGL_NONE.
3 EGL. ИНИЦИАЛИЗАЦИЯ OPENGL ES 14

Пример такого массива:

Шаблон конфигурации
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
};

Возможные ключи конфигурации


EGL_ALPHA_SIZE
EGL_ALPHA_MASK_SIZE
EGL_BIND_TO_TEXTURE_RGB
EGL_BIND_TO_TEXTURE_RGBA
EGL_BLUE_SIZE
EGL_BUFFER_SIZE
EGL_COLOR_BUFFER_TYPE
EGL_CONFIG_CAVEAT
EGL_CONFIG_ID
EGL_CONFORMANT
EGL_DEPTH_SIZE
EGL_GREEN_SIZE
EGL_LEVEL
EGL_LUMINANCE_SIZE
EGL_MAX_PBUFFER_WIDTH
EGL_MAX_PBUFFER_HEIGHT
EGL_MAX_PBUFFER_PIXELS
EGL_MAX_SWAP_INTERVAL
EGL_MIN_SWAP_INTERVAL
EGL_NATIVE_RENDERABLE
EGL_NATIVE_VISUAL_ID
EGL_NATIVE_VISUAL_TYPE
EGL_RED_SIZE
EGL_RENDERABLE_TYPE
EGL_SAMPLE_BUFFERS
EGL_SAMPLES
EGL_STENCIL_SIZE
EGL_SURFACE_TYPE
EGL_TRANSPARENT_TYPE
EGL_TRANSPARENT_RED_VALUE
EGL_TRANSPARENT_GREEN_VALUE
EGL_TRANSPARENT_BLUE_VALUE
...

+ еще существуют вендорозависимые ключи, такие как EGL_COVERAGE_BUFFERS_NV (


сглаживание для чипов Tegra и других ).

Для нас важными являются только несколько ключей:


EGL_RED_SIZE – бит на красный канал
EGL_GREEN_SIZE – бит на зеленый канал
EGL_BLUE_SIZE – бит на синий канал
EGL_ALPHA_SIZE – бит на альфа канал
EGL_DEPTH_SIZE – глубина Z буфера
3 EGL. ИНИЦИАЛИЗАЦИЯ OPENGL ES 15

EGL_RENDERABLE_TYPE – API поддерживаемые в данной конфигурации.


Значение – битовая маска, так как одной и той же конфигурации может соответствовать
несколько API.
OpenGL ES 2.0 соответствует значение 4
EGL_SAMPLE_BUFFERS – Поддержка антиалайзинга
EGL_SAMPLES – количество семплов на пиксель
EGL_NONE – завершение списка

Как это всё работает


// Создаем шаблон конфигурации с минимальными требуемыми параметрами
int[] configSpec = {
EGL10.EGL_RED_SIZE, 5, // минимум 16битный цвет
EGL10.EGL_GREEN_SIZE, 6,
EGL10.EGL_BLUE_SIZE, 5,
EGL10.EGL_DEPTH_SIZE, 16, // Глубина Z буффера минимум 16бита
EGL10.EGL_RENDERABLE_TYPE, 4, // поддержка GLES20
EGL10.EGL_SAMPLE_BUFFERS, 1, // поддержка антиалайзинга
EGL10.EGL_SAMPLES, 2, // минимум 2 семпла
EGL10.EGL_NONE
};

// Запрашиваем список подходящих конфигураций


//
mValue = new int[1];
egl.eglChooseConfig(display, configSpec, null, 0,mValue);
int numConfigs = mValue[0]; // получаем количество конфигов подходящих под наше опи
сание.
// так как шаблон конфигурации задает МИНИМАЛЬНЫЕ требования
//то в данном случае в список попадут конфигурации и с 32битным цветом и со сглажив
анием например на 4 ( или 8 ) сэмплов.

if(numConfigs <= 0) {
EGLConfig] configs = new EGLConfig[numConfigs];
egl.eglChooseConfig(display, configSpec, configs, numConfigs,mValue); // Полу
чаем список конфигураций.
// Теперь у нас есть заполненный массив конфигураций configs
} else {
//Конфигураций соответствующих шаблону не найдено.
}

Важно:

Когда более чем одна конфигурация буфера кадра соответствует шаблону, возвращается
список конфигураций.

Список сортируется в соответствии со следующими правилами приоритета, которые при-


меняются в порядке:

1. по EGL_CONFIG_CAVEAT, в следующем порядке:

EGL_NONE,
EGL_SLOW_CONFIG и
EGL_NON_CONFORMANT_CONFIG.

Например разработчик устройства ( драйвера ) пометил определенные конфигурации как


3 EGL. ИНИЦИАЛИЗАЦИЯ OPENGL ES 16

медленные или не рекомендуемые – они окажутся в конце списка.

2. по EGL_COLOR_BUFFER_TYPE, в порядке:

EGL_RGB_BUFFER,
EGL_LUMINANCE_BUFFER ( монохромный ).

3. по сумме числа бит всех каналов RGBA ( или глубине EGL_LUMINANCE_SIZE ).

То есть сначала в списке идут конфигурации с максимальной глубиной цвета и далее в


порядке уменьшения.
На это стоит обратить особое внимание, так как если вы запросили конфигурацию RGB565
без альфа-канала то первыми в списке скорее всего будут конфигурации RGBA8888 ( при
одинаковых остальных параметрах ), так как сумма бит всех каналов в них больше.

4. EGL_BUFFER_SIZE в порядке возрастания

( то есть сначала будут конфигурации с минимальным значением ).

5. EGL_SAMPLE_BUFFERS в порядке возрастания.

6. EGL_SAMPLES в порядке возрастания.

7. EGL_DEPTH_SIZE в порядке возрастания.

8. EGL_STENCIL_SIZE в порядке возрастания.

9. EGL_ALPHA_MASK_SIZE в порядке возрастания.

10. EGL_NATIVE_VISUAL_TYPE ( тут в зависимости от реализации, обычно одно зна-


чение ).

10. EGL_CONFIG_ID в порядке возрастания (последняя опция сортировки, гарантирую-


щая уникальность).

Сортировка не производится по ключам:

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

Пример. Выводит на экран все доступные конфигурации для OpenGL ES 2.0

Листинг 3: Доступные конфигурации OpenGL ES 2.0


import android.os.Bundle;
import android.view.Window;
import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

import javax.microedition.khronos.egl.*;

public class MyActivity extends Activity {


final int EGL_COVERAGE_BUFFERS_NV = 0x30E0;
final int EGL_COVERAGE_SAMPLES_NV = 0x30E1;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN);

TextView textv = new TextView(this);


// TextView c ScrollView для отображения результата

LinearLayout.LayoutParams blp = new LinearLayout.LayoutParams(


LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.MATCH_PARENT
);
ScrollView scrollv = new ScrollView(this);
scrollv.setLayoutParams(blp);
scrollv.addView(textv);
this.addContentView(scrollv,blp);

EGL10 Egl = (EGL10) EGLContext.getEGL();


// получаем враппер Egl

EGLDisplay EglDisplay = Egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);


// получаем ссылку на дисплей

int[] version = new int[2]; // массив для получения версии EGL

Egl.eglInitialize(EglDisplay, version); // Инициализация EGL

int[] configSpec = { // шаблон конфигурации


EGL10.EGL_RENDERABLE_TYPE, 4, // поддержка GLES20
EGL10.EGL_NONE // конец
};
int[] mValue = new int[1];

Egl.eglChooseConfig(EglDisplay, configSpec, null, 0,mValue);


// получаем колличество конфигураций подходящих под шаблон

int numConfigs = mValue[0];


EGLConfig[] configs = new EGLConfig[numConfigs];
int[] num_conf = new int[numConfigs];

Egl.eglChooseConfig(EglDisplay, configSpec, configs, numConfigs,mValue);


// получаем массив конфигураций
3 EGL. ИНИЦИАЛИЗАЦИЯ OPENGL ES 18

String text ="EGL␣version␣"+version[0]+"."+version[1]+"\n";


text+= printConfigs(configs,EglDisplay,Egl)+"\n";
textv.setText(text);
}

private String printConfigs(EGLConfig] conf,EGLDisplay EglDisplay,EGL10 Egl) {


String text="";
for(int i = 0; i < conf.length; i++) {
int[] value = new int[1];
if (conf[i] != null)
{
text+="====␣Config␣є"+i+"␣====\n";
Egl.eglGetConfigAttrib(EglDisplay, conf[i], EGL10.EGL_RED_SIZE, value);
text+="EGL_RED_SIZE␣=␣"+value[0]+"\n";
Egl.eglGetConfigAttrib(EglDisplay, conf[i], EGL10.EGL_GREEN_SIZE, value
);
text+="EGL_GREEN_SIZE␣=␣"+value[0]+"\n";
Egl.eglGetConfigAttrib(EglDisplay, conf[i], EGL10.EGL_BLUE_SIZE, value)
;
text+="EGL_BLUE_SIZE␣=␣"+value[0]+"\n";
Egl.eglGetConfigAttrib(EglDisplay, conf[i], EGL10.EGL_ALPHA_SIZE, value
);
text+="EGL_ALPHA_SIZE␣=␣"+value[0]+"\n";
Egl.eglGetConfigAttrib(EglDisplay, conf[i], EGL10.EGL_DEPTH_SIZE, value
);
text+="EGL_DEPTH_SIZE␣=␣"+value[0]+"\n";
Egl.eglGetConfigAttrib(EglDisplay, conf[i], EGL10.EGL_SAMPLE_BUFFERS,
value);
text+="EGL_SAMPLE_BUFFERS␣=␣"+value[0]+"\n";
Egl.eglGetConfigAttrib(EglDisplay, conf[i], EGL10.EGL_SAMPLES, value);
text+="EGL_SAMPLES␣=␣"+value[0]+"\n";
Egl.eglGetConfigAttrib(EglDisplay, conf[i], EGL_COVERAGE_BUFFERS_NV,
value);
text+="EGL_COVERAGE_BUFFERS_NV␣=␣"+value[0]+"\n";
Egl.eglGetConfigAttrib(EglDisplay, conf[i], EGL_COVERAGE_SAMPLES_NV,
value);
text+="EGL_COVERAGE_SAMPLES_NV␣=␣"+value[0]+"\n\n";
} else { break; }
}
return text;
}
}
4 МЕТОДЫ GLSURFACEVIEW 19

3.3 Про антиалайзинг ( сглаживание )


Антиалайзинг включается на уровне драйвера.
Использование поверхностей с EGL_SAMPLE_BUFFERS задает режим MSAA EGL_SAMPLES
задает количество семплов на пиксель, например, при 4 получаем режим MSAAx4.
В процессе рендеринга управлять MSAA-сглаживанием нельзя.
EGL_COVERAGE_BUFFERS_NV и EGL_COVERAGE_SAMPLES_NV задают режим CSAA
аналогичным образом.
Некоторые чипы, Tegra например, могут работать только с CSAA антиалайзингом.
В процессе рендеринга возможно управлять CSAA.
Но я бы не советовал использовать ни тот ни другой режим – а использовать FXAA.
Он намного "легче"в плане вычислений, просчитывается за один проход постобработки и
дает лучший визуальный результат.
FXAA возможен только для OpenGL ES 2.0 и последующих редакций.
Остался один момент.
Вспомните строчку из GLSurfaceView:
...
holder.setFormat(PixelFormat.RGB_565);
...

Что будет если мы попытаемся инициализировать GLSurface в режиме RGBA8888 с на


поверхности с PixelFormat.RGB_565?
Варианты:
1. система приведет RGBA8888 к RGB565 и мы получим 16битный цвет без прозрачности.
2. система будет нормально отображать RGBA8888, наплевав на RGB565.
3. Все с треском упадет.
На самом деле может случится любой из перечисленных вариантов, поэтому настоятельно
рекомендуется привести SurfaceView к формату GLSurface.
Привести SurfaceView нужному формату можно приблизительно таким способом:
...
GLSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
...

И еще, никогда не нужно хомячить и пытаться задействовать максимальную конфигура-


цию, так как за всё приходится расплачиваться производительностью.

Вот и все про инициализацию OpenGL ES 2.0.


Далее детальное рассмотрение GLSurfaceView, его методов и субклассов.

4 GLSurfaceView. Улучшенный каркас приложения


с использованием GLSurfaceView
Для инициализации OpenGL ES и как основу приложения в дальнейшем буду использо-
вать GLSurfaceView.

Причины этого или почему не написать самим:


1. В GLSurfaceView код уже отлажен и проверен.
2. Некоторые разработчики вносят свои изменения в стандартный GLSurfaceView для улуч-
шения каких-либо показателей или работоспособности конкретного железа.
3. С GLSurfaceView можно и удобно работать как с любым другим View/Виджетом.
4. Не будем изобретать велосипед.
4 МЕТОДЫ GLSURFACEVIEW 20

GLSurfaceView включает в себя следующие интерфейсы:


EGLConfigChooser – Интерфейс для выбора EGLConfig конфигурации из списка воз-
можных конфигураций.
EGLContextFactory – Интерфейс для своей реализации eglCreateContext и eglDestroyContext
вызовов.
EGLWindowSurfaceFactory – Интерфейс для своей реализации eglCreateWindowSurface
and eglDestroySurface вызовов.
GLWrapper – враппер GL
Renderer – Интерфейс визуализации.
В данном каркасе понадобятся EGLConfigChooser и Renderer интерфейсы и сам EGLConfigChooser.

4.1 Методы GLSurfaceView


void onPause() – проинформировать GLSurfaceView о событии onPause в активити.

void onResume() – проинформировать GLSurfaceView о событии onResume в активити.

void queueEvent(Runnable r) – поставить в очередь в потоке рендеринга.


Далее про этот момент подробнее.

void requestRender() – запросить рендеринг кадра

void setEGLConfigChooser(GLSurfaceView.EGLConfigChooser configChooser) –


использовать кастомный EGLConfigChooser.

void setEGLConfigChooser(boolean needDepth) – установить конфигурацию 16бит RGB


с буфером глубины в 16 бит ( или как можно ближе к этому значению ) в зависимости от
needDepth.

void setEGLConfigChooser(int redSize, int greenSize, int blueSize, int alphaSize, int depthSize,
int stencilSize) – установить конфигурацию с определенными значениями глубины цвета на
канал, depth-буфера и глубины буфера трафорета.

void setEGLContextClientVersion(int version) – установить версию Контекста OpenGLES,


version = 1 для OpenGLES1.0-1.1 и version = 2 для OpenGES2.0

void setRenderMode(int renderMode) – установить режим смены кадров.


Возможны два варианта:
RENDERMODE_CONTINUOUSLY – автоматически обновлять экран.
RENDERMODE_WHEN_DIRTY – обновлять по запросу ( вызовом requestRender() ).
Теперь вопрос, сколько кадров в секунду при автоматическом обновлении?
Тут все сильно зависит от устройства.
Например максимальное возможное количество кадров, или стабилизация с аппаратным об-
новлением экрана. Что не всегда хорошо.
Мы же пишем для МОБИЛЬНЫХ устройств и где есть возможность использовать 30 кадров
в секунду вместо 60, но позволить устройству на час-полтора дольше проработать. . .
Так что будем стабилизировать кадры вручную.

void setRenderer(GLSurfaceView.Renderer renderer) – установить свою реализацию Renderer.


Метод обязателен для инициализации GLSurfaceView.
4 МЕТОДЫ GLSURFACEVIEW 21

И последний метод защищает GLContext когда приходит событие OnPause:


setPreserveEGLContextOnPause(boolean preserveOnPause)
По умолчанию значение False. Доступен только на API11 и выше.
Дело в том что не все GPU могут работать с несколькими контекстами одновременно.
Из-за этого приходилось выгружать ( разрушать ) старый контекст и на его месте создавать
новый для новой задачи.
GPU поддерживающие ES 2.0 поддерживают работу с несколькими контекстами.
Для того что бы GLSurfaceView не разрушал контекст в случае события OnPause для API11
и выше нужно включить setPreserveEGLContextOnPause(true);
Для API меньше 11 режим включится автоматически если GLES больше или ровна 2.0.

Вот и все методы которые нам могут понадобится в GLSurfaceView.

4.2 Реализация интерфейса EGLConfigChooser


В EGLConfigChooser для работы нам нужно реализовать один метод –
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display), который возвращает вы-
бранный конфиг в GLSurfaceView.
Пример реализации EGLConfigChooser для выбора RGB888 без буфера глубины и с
MSAA-сглаживанием ( чего нельзя добиться методами GLSurfaceView):

Листинг 4: Пример реализации EGLConfigChooser (RGB888+MSAA)


import android.opengl.GLSurfaceView;
import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLDisplay;

public class Config2D888MSAA implements GLSurfaceView.EGLConfigChooser {


private int[] Value;
public EGLConfig chooseConfig(EGL10 egl, EGLDisplay display) {
Value = new int[1];
int[] configSpec = { // задаем шаблон спецификации
EGL10.EGL_RED_SIZE, 8,
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_RENDERABLE_TYPE, 4,
EGL10.EGL_SAMPLE_BUFFERS, 1,
EGL10.EGL_SAMPLES, 2,
EGL10.EGL_NONE
};
if (!egl.eglChooseConfig(display, configSpec, null, 0,Value)) {
throw new IllegalArgumentException("RGB888␣eglChooseConfig␣failed");
}
int numConfigs = Value[0];
if (numConfigs <= 0) { // Если подходящих кофигураций RGB888 нет, то пробуем
получить RGB888 или на худой конец RGB565 без сглаживания
configSpec = new int[] {
EGL10.EGL_RED_SIZE, 5,
EGL10.EGL_GREEN_SIZE, 6,
EGL10.EGL_BLUE_SIZE, 5,
EGL10.EGL_RENDERABLE_TYPE, 4,
EGL10.EGL_NONE
};
if (!egl.eglChooseConfig(display, configSpec, null, 0, Value)) {
throw new IllegalArgumentException("RGB565␣eglChooseConfig␣failed");
}
numConfigs = Value[0];
4 МЕТОДЫ GLSURFACEVIEW 22

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]; // возвращаем конфиг
}
}

Более подробное описание выбора конфигураций было в предыдущем уроке.

4.3 Реализация Renderer


Теперь нам нужно сделать свою реализацию интерфейса Renderer.
Для работы нам необходимо реализовать три метода:
onDrawFrame() – отрисовка самого кадра
onSurfaceChanged(GL10 glUnused, int width, int height) – создание GLSurface. Будет
вызываться например при смене ориентации экрана и первоначальной загрузки. Нужные
параметры – int width, int height, ширина(x) и высота(y) соответственно.
onSurfaceCreated(GL10 glUnused, EGLConfig config) – вызовется до создания GLSurface,
но после инициализации OpenGL.

Нужный нам параметр – EGLConfig config.


То есть конфигурация GLSurface, на основе которой мы можем решить какие ресурсы под-
гружать и т.д.
Если повторно сработал этот метод – считайте система потеряла GLContext и вместе с ним
все загруженные текстуры, массивы и настройки.
Требуется перезагрузка всех ресурсов.

Инициализацию ресурсов можно проводить в onSurfaceChanged ( если ресурсы зависят


от геометрии экрана ) или в onSurfaceCreated если проект небольшой.
Очень желательно загрузить ВСЕ ресурсы сразу ( конечно если это позволяет расход памяти
и т.д. ), иначе лучше будет выполнять подгрузку отдельным потоком, так как блокировать
Render-поток не самая хорошая идея.
При инициализации и работе OpenGL эти методы вызываются в следующей последова-
тельности:

onSurfaceCreated =⇒ onSurfaceChanged =⇒ onDrawFrame =⇒ ... =⇒ onDrawFrame =⇒ ...

Если происходит переворот экрана без потери GLContext, то метод onSurfaceCreated не


вызывается.
onSurfaceCreated при загрузке и восстановлении GLContext.

К реализации GLSurfaceView.Renderer я добавил метод onTouchEvent для примера обра-


ботки ввода с экрана.
В реальном приложении еще потребуется как минимум метод onPause, для того чтобы сохра-
нить текущее состояние, так как не факт, что система не закроет приложение после onPause,
и когда-нибудь произойдет onResume.
4 МЕТОДЫ GLSURFACEVIEW 23

Ещё сделал конструктор для передачи ссылки на контекст приложения.

Листинг 5: Пример реализации интерфейса Renderer


import android.content.Context;
import android.opengl.GLSurfaceView;
import android.view.MotionEvent;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;

public class GLRender implements GLSurfaceView.Renderer {

private Context cnx;

public GLRender(Context context) {


this.cnx = context;
}

public void onDrawFrame(GL10 glUnused) {


// Выполняем весь рендеринг тут.
}

public void onSurfaceChanged(GL10 glUnused, int width, int height) {


GLES20.glViewport(0, 0, width, height); // устанавливаем вьюпорт по размеру
GLSurface.
}

public void onSurfaceCreated(GL10 glUnused, EGLConfig config) {


}

public void onTouchEvent(final MotionEvent event) {


}
}

ВАЖНО: ПОТОКИ.
Класс Renderer выполняется в ОТДЕЛЬНОМ потоке, а не в UI или главном потоке,
для обеспечения лучшего быстродействия.
GLSurfaceView отрабатывает в потоке UI так как является View.

Для синхронизации Renderer c GLSurfaceView и остальными потоками использу-


ется метод GLSurfaceView.queueEvent().

Пример
public boolean onTouchEvent(final MotionEvent event) {
glSurfaceView.queueEvent(new Runnable() {
public void run() {
render.onTouchEvent(event);
}
} );
return true;
}
4 МЕТОДЫ GLSURFACEVIEW 24

4.4 Стабилизация FPS


Получить refresh-rate ( аппаратный ) экрана можно таким способом:

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.

Листинг 6: Пример реализации стабилизации FPS


private int FPS = 30; // 30 кадров в секунду
private Boolean RPause = false; // флаг паузы
. . .
void reqRend(){
mHandler.removeCallbacks(mDrawRa); // убиваем на всякий случай все отложенные вы
зовы mDrawRa
if(!RPause) {
mHandler.postDelayed(mDrawRa, 1000 / FPS); // запускаем Runnable с задержкой
glSurfaceView.requestRender(); // рендерим кадр
}
}
. . .
private final Runnable mDrawRa = new Runnable() {
public void run() {
reqRend();
}
};
. . .
@Override
protected void onResume() {
super.onResume();
glSurfaceView.onResume();
RPause = false;
reqRend(); // запускаем рендеринг
}
. . .
4 МЕТОДЫ GLSURFACEVIEW 25

4.5 Реализация Activity


И теперь полный код Activity ( используется платформа Android 4.0.4 (API15), работает
с версии 2.2(API8) )

Листинг 7: полный код Activity


import android.app.Activity;
import android.graphics.PixelFormat;
import android.opengl.GLSurfaceView;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.Window;
import android.view.WindowManager;

public class MyActivity extends Activity {


private GLSurfaceView glSurfaceView;
private GLRender render;
private Config2D888MSAA ConfigChooser;
private Handler mHandler = new Handler();
private Boolean RPause = false; // флаг паузы
private int FPS = 30; // кадров в секунду

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

requestWindowFeature(Window.FEATURE_NO_TITLE); // убираем заголовок


getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
); // устанавливаем полно-экранный режим

glSurfaceView = new GLSurfaceView(this);

if (Build.VERSION.SDK_INT > 10) {


glSurfaceView.setPreserveEGLContextOnPause(true);
// если версия API выше 11 и выше устанавливаем защиту GLContext
}
glSurfaceView.getHolder().setFormat(PixelFormat.RGBA_8888);
glSurfaceView.setEGLContextClientVersion(2);
glSurfaceView.setEGLConfigChooser( ConfigChooser = new Config2D888MSAA() );
//используем свою реализацию EGLConfigChooser

render = new GLRender(this); // инициализируем свою реализацию Renderer


glSurfaceView.setRenderer(render);
glSurfaceView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // устанавл
иваем смену кадров по вызову
setContentView(glSurfaceView); // ставим наш glSurfaceView как корневой View
активити.
}

void reqRend(){
mHandler.removeCallbacks(mDrawRa);
if (!RPause){
mHandler.postDelayed(mDrawRa, 1000 / FPS); // отложенный вызов mDrawRa
glSurfaceView.requestRender();
}
}
5 ОТСТУПЛЕНИЕ А: ДВИЖКИ, ОПТИМИЗАЦИЯ... 26

private final Runnable mDrawRa = new Runnable() {


public void run() {
reqRend();
}
};

@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

Всегда лучше подход "расширяемость", а не "все в одном".


Обилие никому не нужных функций уже "убивало"␣многие большие проекты.
Вспомните ACDSee или Nero.
Которые полюбили за то, что они были безотказные и быстрые.
Теперь они могут все, включая поиск на винте, проигрывать видео и приносить тапочки.
Только не все форматы картинок уже открывают и не все диски пишут...
В общем, поверьте, что если бы был "один самый правильный способ все сделать", то он уже
был бы в стандарте API.
Конечно можно сделать максимально универсальный способ, но это будет ни рыба ни мясо.

В данном случае есть способ сделать что-то только тремя способами:


1. Быстро работает. Реализация под конкретную задачу и железо.
2. Просто.Не замарачиватся с оптимизациями.Делается быстро, скорость средняя.
3. Универсально. Универсальности нет ( так как реально перебрать все варианты не полу-
чится), работает медленно, пишется дооолго.

Про оптимизацию:
Оптимизировать нужно только то, что реально влияет на производительность конкретной
задачи/приложения.
Допустим вы ускорили загрузку и инициализацию в два раза, потратив на это три месяца.
Раньше приложение грузилось за 0.8 секунды, а теперь стало за 0.4
То есть вместо того, чтобы сделать приложение быстрее, вы занимались хрен знает чем, что
ни один пользователь не разглядит.
Другое дело, если раньше загрузка и инициализация занимала пол минуты. Пользователей
это нервировало.
То есть оптимизируем дополнительно только то, что "критично", а не пытаемся сразу сделать
самый быстрый код везде.
Даже у больших компаний не хватает времени для оптимизации всего, что говорить про
инди или маленькие конторки.
Тут все скажут – это дураку понятно, но я реально встречал много случаев когда так и
происходило.
Программист вместо того, чтобы перейти к следующей задачи продолжал играть с предыду-
щей под предлогом оптимизации, котороя ей нафиг не нужна была.

5.2 Для любителей пихать ООП куда нужно и не очень.


( в основном про Java, а также справедливо для C#, lua и т.д. )
Не пытайтесь выстроить стройную цепочку классов до конца(!) разработки(!) всего ос-
новного функционала.
Это бессмысленно. Все равно все будете переписывать по сто раз.
Не оборачиваете в классы простые типы.
Это прокатывает на C++ и отвратительно на языках со сборщиком мусора.
Используйте простые типы!
Например:
class Vertex {
Float x, y, z;
}
Vertex[] model = new Vertex[10000];

За "это"нужно бить ногами.


5 ОТСТУПЛЕНИЕ А: ДВИЖКИ, ОПТИМИЗАЦИЯ... 28

Float – объект, соответственно у него есть ссылка и счетчик ссылающехся на него ( для
сборщика мусора )
Ссылка как минимум 32бит, счетчик как минимум 32бит.
То есть, чтобы записать 32 бита ( данных ), мы израсходовали 96. ( o_0 )
Далее еще Vertex[] model добавит свои 64.
То есть получается что Float x, y, z весит 352бита вместо 96бит.

Другой пример:
Vertex[3][10000]
Vertex[10000][3]

Кажется разницы нет никакой?

В первом случае, мы имеем три массива из десяти тысяч элементов.


Во втором, десять тысяч ссылок на массивы из трех элементов.
Всё это повлечет бешенство сборщика мусора, вполне заметное торможение и дикий расход
памяти ( плевок в сторону U. )

В реальных приложения приходится реализовывать свой сборщик, так как ссылка на


десятиметровую текстуру занимает 32бита, и ждать когда родной сборщик сподобится нет
возможности ( он убивает в первую очередь крупные объекты, и нет гарантии что маленький
прихлопнет ).

5.3 Запас прочности и производительности


Всегда рассчитывайте запас памяти и производительности при проектировании минимум
раза в полтора.
Лучше в два.

Из старой книжки ( уже не помню названия, год 70∼):

Мосты в США:

Такомский мост – рухнул. Был рассчитан по всем канонам.


Золотые Ворота – стоят как ни всем не бывало.
Ответ прост – один был рассчитан под максимальные теоретические нагрузки, другой под
максимальные теоритические нагрузки * 2.
Какой из них говорить я думаю смысла не имеет...

Конечно всего не предусмотришь, но нужно стремится =).


И считать, что от первоначальных проектных как минимум раза в полтора потребности
вырастут.
Так и вы не рассчитывайте, что если у устройства хотя бы 512 памяти, то вы сможете на
них развернутся.
Ставьте всегда планку расхода и не забывайте, что вы в системе не одни, и другие приложе-
ния и процессы не поддержат вас в захватывании всей памяти.
5 ОТСТУПЛЕНИЕ А: ДВИЖКИ, ОПТИМИЗАЦИЯ... 29

5.4 Ненужный код


Код для никого.
Возьмем для примера ShaderProgram.java из libgdx.
Для его использования ( 922 строчки ) нужно точно и хорошо знать, что такое униформы и
атрибуты.
Кто знает это, может написать тоже самое за 10 строчек, для кого этот код?
Кто не знает – и так не напишет, кто знает – напишет лучше.
Для кого и чего это?
Этот код для никого.

5.5 Движки или что под этом понимается


Универсальных быстрых движков НЕТ.
И не может быть.
Каждый быстрый движок заточен под что-то одно.
Если создатели захотели универсальности – то на выходе вы получите сложность, плохую
скорость, плохую расширяемоть и минимум переносимости.
Прописать все возможные моменты даже простого "универсального"рендеринга "спрайта"не
смогли даже титаны как UE.
За 15 лет... Что говорит о том, что общего решения нет и о том, что законы в компьютерной
графике меняются слишком быстро.
Алгоритм, что еще два года назад считался самым быстрым, сегодня – прошлый век, и толь-
ко будет замедлять вашу программу/игру под нынешние GPU.

Общий взгляд на движки под Android:

1. Титаны.
UE, FrostByte, GameBro, Q3,4,5∼ и т.д.
Плюсы:
100500 игр на них.
Реальная мультиплатформенность.
Скорость, стабильность.
Заточены под работу большой группы людей.
Очень дорого. Как сам движок так и поддержка.
Зато очень хорошо сокращают время на разработку, которая часто выходит дороже стоимо-
сти этих движков.
Маленьким конторам и инди обычно не нужно столько наворотов за такую цену.
И обычно даже движки ААА класса не без патологий, так как они тянут за собой много
хвостов.

2. 2D движки бесплатные.
Бесплатные – на безрыбье рак рыба.
Часть возможностей реализована через попу, часто создается впечатление что это курсовик
студента, который почему-то решил это допиливать.
Но не все так плохо.
Есть вполне годные под определенные задачи.
Обычно хоть и с доступными исходниками – разработчиков 1-3 человека.
Как заканчивается энтузиазм – опенсорс и через пол года...

3. 2D движки платные.
6 ОТСТУПЛЕНИЕ B: ПРИМИТИВЫ И ПРОЧЕЕ 30

Обычно больше возможностей, даже иногда реализуют дополнительный функционал по за-


просу за/без денег.
Часто это компании сами пишут игрушки и заодно продают "движок".
Вообще смысла нет если нет исходников.

4. Движки рекламные.
Когда привлечение пользователей и программистов не из-за качества или использования – а
из-за рекламы.
DarkBasiс,Unity3D и т.д.

DarkBasiс сдох. Он был совсем убог.

Юнити на пике популярности.


Но готовых проектов на нем пока что мало ( в сравнении с другими большими движками ).
Умирать не собирается, сообщество большое.
+ вполне годное расширяемое IDE.
Из недостатков – большой вес пустого проекта, требовательность к памяти.

5. Конструктуры.
Ну что про них сказать?
Реализовали разработчики – используйте.
Не реализовали – не используйте.
У продвинутых присутствует возможность расширения.
Выбор для тех кто слаб в программировании.

Выбор движка:

Видели "свинок"от создателей AngryBirds?


Затормозить двухядерники на простейших просчетах не каждому дано...
Про расход памяти вообще нужно молчать...
Это конкретный пример как НЕ надо выбирать платформу.

Выбирайте движок под задачу, а не задачу под движок.

Когда машину ремонтируете, выбираете ключ под гайку или берете любимый ключ и хо-
дите вокруг машины, думая, что открутить?
Обычно разработчики подробно описывают возможности, так что подобрать то что надо не
составит труда.

6 Примитивы OpenGL Es 2.0.


Матрицы, вектора и преобразования в OpenGL.
Система координат под Android.
Так как рассматривать текстуры без некоторых знаний бесполезно - привожу их ( знания
=) ) здесь.
В основном эта информация интересна для тех, кто не был знаком ни с какой редакцией
OpenGL ( просьба этим людям внимательно и вдумчиво прочитать эту часть, так как тут
описаны основополагающие вещи )...
Но и остальным можно чуть-чуть вспомнить геометрию и посмотреть на отличая OpenGL
6 ОТСТУПЛЕНИЕ B: ПРИМИТИВЫ И ПРОЧЕЕ 31

ES 2.0 от предыдущих версий.

6.1 Примитивы OpenGL ES 2.0


”Сцена OpenGL” – так как в OpenGL мы не рисуем в прямом смысле слова, а задаем
параметры ( такие как местоположение, цвет, размер и т.д ) примитивов, то в дальнейшем
буду говорить не "рисуем"и "строем сцену"( да и сам процесс похож на построение сцены –
расставить декорации, актеров, свет и т.д.).

Примитивы – ( атомы, минимально-возможные ) элементы построения сцены в OpenGL.

В OpenGL ES 2.0 используется три вида примитивов: Треугольник(Triangle), Линия (Line)


и Точечные спрайты ( Point Sprites ).

(!) Замечание: примитива Quard ( прямоугольник ) из прошлых версий в OpenGL ES


2.0+ НЕТ, да и он не нужен, так как, на самом деле, он состоит из двух треугольников.

В реальных задачах в основном используется примитив Triangle (Треугольник), реже


Point Sprites (Точечные спрайты) и совсем редко Line(Линия).

Немного истории:
На заре становления графических ускорителей разные производители пытались использовать
некоторые другие примитивы, такие как квадратичные поверхности ( Nvidia NV1 ) и четы-
рёхугольники (Sega Saturn), что несомненно было бы шагом назад для всей компьютерной
графики.
Большая сложность моделирования, огромная сложность вычислений по тем временам ( чи-
пы 92-95года не могли аппаратно вычислять те уравнения с нужной точностью, что давало
огромные искажения – например при наложении текстур ), плохое качество картинки.
Но тут вмешалась фирма Microsoft, сделав API Dicert3D с поддержкой только треугольни-
ков ( и Quard соответственно ), что определило дальнейшие развитие GPU ( и слава богу
), да и остальное программное обеспечение того времени такое как 3D Studio ( еще без
MAX ) и LightWave 3D работали с треугольниками. В некоторых системах рендеринга до
сих пор используется большее количество примитивов ( в основном в совтверных, то есть
просчитываемых на не GPU а на CPU ), таких как бесконечная плоскость, идеальный шар (
овал ) и т.д.

6.1.1 ПРИМИТИВ Triangle (Треугольник)


Треугольник как примитив выбран не случайно.

Треугольник состоит из:


* Трех вершин.
* Трех ребер.
* Трех углов.

В OpenGL примитив Triangle задается с помощью трех точек – вершин ( Vertex2 ).

2
Другие примитивы тоже задаются с помощью точек, их тоже называют по аналогии с Triangle – вершины
( Vertex ).
6 ОТСТУПЛЕНИЕ B: ПРИМИТИВЫ И ПРОЧЕЕ 32

Как помним из геометрии однозначно задать плоскость можно с помощью трех точек не
лежащих на одной прямой.
Теперь представим плоскость проходящую через вершины треугольника и ограниченную его
ребрами. Это и есть наш примитив Triangle.

Плюсы выбора Triangle:


* С ним удобно работать при моделировании, он является самым простым полигоном(многоугольником),
разработки ( 3D редакторы ) ориентированны на полигональную ( треугольную ) графику.
* Каждый полигон(многоугольник) можно разбить до треугольников.
* Для задания треугольника требуется только три точки.
* ”простая” математика при расчетах.

В дальнейшем треугольные примитивы буду называть ”полигоны”, так как именно такое
обозначение будет встречаться почти во всех статьях и книгах.

В OpenGLES 2.0 полигоны можно задать тремя способами:

GL_TRIANGLES

В этом режиме каждые три вершины ( Vertex ) задает отдельный треугольник.


Лицевая или обратная сторона задается направлением обхода ( объявления ) вершин – по
часовой или против часовой стрелки.

GL_TRIANGLE_STRIP

Первые три вершины задают один полигон.


Каждая последующая вершина задает новый полигон, используя для этого две предыдущие
вершины.
Как видно из рисунка V0V1V2 задают первый полигон, V1V2V3 – второй и т.д.

GL_TRIANGLE_FAN

Первые три вершины задают один полигон.


Следующий полигон задается вершиной V0,вершиной Vn-1 и новой вершиной Vn.
6 ОТСТУПЛЕНИЕ B: ПРИМИТИВЫ И ПРОЧЕЕ 33

6.1.2 ПРИМИТИВ Line (Линия)

В моих примерах примитив Line я не использую, так что детально его рассматривать не
буду ( так как не было цели написать библию OpenGL ES ).
Скажу только, что тут все аналогично полигону, только задается с помощью двух вершин
(точек).

6.1.3 ПРИМИТИВ Point Sprites (Точечные спрайты)


У примитива Point Sprite есть только один режим построения, так как каждый спрайт
задается только одной вершиной.
Существуют существенные ограничения на размер, текстуру и трансформацию.
PointSprites всегда расположены поверхностью к камере ( являются билбордами ), имеют
ограничения на размер ( обычно минимум 64 пиксела ) и т.д.
Главное достоинство – мегабыстрые.
Реальная производительность приближается к теоретическим для GPU ( да, да, те самые
10-50 миллионов мифических треугольников в секунду =) ).
В основном используется для построения систем частиц, хотя никто не мешает рисовать
таким методом и тайловую графику, например.
Подробнее про них в отдельном уроке.

Вообще, "строго"говоря нынешнюю 3D графику можно отнести к векторной, так как все
примитивы задаются с помощью векторов =).

Вот и все по примитивам.


6 ОТСТУПЛЕНИЕ B: ПРИМИТИВЫ И ПРОЧЕЕ 34

6.2 Матрицы, вектора. Преобразования в OpenGL


Вектор — упорядоченный набор из некоторого количества действительных чисел.
Проще говоря вектор это отрезок заданный двумя точками ( как все отрезки ), у которого
одна точка - начало координат (следовательно все нули), другая задает длину и направление.
То есть направление вектора всегда из начала координат до точки, задающей его положение
и направление.
Соответственно, его можно задать одной точкой.
Например наша вершина ( точка ) является вектором, так как это набор из нескольких ко-
ординат V(x,y) для 2D или V(x,y,z) для 3D соответственно.
Векторы могут быть начиная от одномерного до n-мерного.

Интересуют нас два свойства вектора – длина ( модуль ) и направление.

Нормализация векторов - получение вектора, направление которого совпадает с исход-


ным, а длина равна единице.
Для большинства расчетов используют нормализованные вектора/нормали.

Нормаль – это вектор перпендикулярный чему либо, в 3D графике в основном это вектор
перпендикулярный поверхности полигона.

В настоящее время название ”нормаль” применяется не только к векторам перпендику-


лярным плоскости, а, например, к картам разнонаправленных векторов – текстуры нормалей
(хотя если строго считать, то это не нормали разнонаправленные, это плоскость с которой
они срисованы кривая =) ).

Единичная нормаль – нормаль, длинна которой равна единице.

6.2.1 Матрицы
В примерах я буду использовать матрицы размера 3x3 и 4x4.

Расписывать действия над матрицами и векторами не буду ( очень много ), тем более в
инете полно хороших статей ( например вот эта, советую освежить память - или наоборот,
узнать много нового =) ). Настоятельно не рекомендую читать википедию по этим вопросам.
( хотя можете попробовать )

Из всех действий я буду использовать умножение вектора на матрицу и матрицу на мат-


рицу. Больше не понадобится.

Главное правило с матрицами ( и с векторами при умножении на матрицу ): M1*M2 !=


M2*M1. То есть умножение матрицы M1 на матрицу M2 не равно умножению матрицы M2
на M1.

Вот и весь необходимый минимум математики который понадобится в будущих уроках.


Так как матрицы начинают проходить на младших курсах в институтах - рекомендую всем
чья пора мучится там не наступила прочитать по этой теме отдельно ( да и в дальнейшем
пригодится, КРОМЕ википедии, уж больно сложно они описывают простые темы.
Зачем вообще матрицы - с помощью них раньше решали системы уравнений, но они очень
хорошо прижились в компьютерной графике так как на нынешних GPU все операции с ними
6 ОТСТУПЛЕНИЕ B: ПРИМИТИВЫ И ПРОЧЕЕ 35

аппаратно-ускоренны, соответственно M0*M1 займет один такт ( ну максимум 2, для совре-


менных ), что для CPU потребовало более 80 операций, да и с помощью матрицы 4x4 можно
описать любые преобразование нашей вершины в трехмерном пространстве.
Если бы мы выполняли эти преобразования последовательно, то это бы заняло приблизи-
тельно в полтора раза больше операций чем в случае с матрицей.

Единичная матрица это матрица у которой по главной диагонали - единицы.


Сверху-слева → вниз-направо.
То есть эта матрица при умножении на которую ничего не меняется.

6.2.2 Преобразования
Есть три основных преобразования:
Translate - перенос
Scale - скалирование
Rotate - поворот

Тут сразу стоит уточнить что происходит на самом деле.


Многие начинающие путают понятия перенос и задача положения в пространстве ( по мно-
гим библиотекам 2D это равнозначные вещи, но не в OpenGL ).

Перенос - это не задача местоположения точки в пространстве, а скорее перенос начала


координат, так как вершины ( Vertex ) у нас векторы.

То есть ( перенос =⇒ поворот ) и ( поворот =⇒ перенос ) НЕ ОДНО И ТОЖЕ.

Так как отсчет ведется от начала координат.

Для каждого действия существует своя матрица.


Описание их тут.
То есть вместо вместо переменных в этих матрицах ставим свое значение и умножаем на
свою матрицу скопленных преобразований.
Не забываем что от смены умножаемых меняется результат.

Для понятного знакомому с 2D преобразования должны идти в следующем порядке:


translate
scale
rotate
6 ОТСТУПЛЕНИЕ B: ПРИМИТИВЫ И ПРОЧЕЕ 36

6.2.3 Система координат в Android


Cистема координат в Android перевернута по оси Y по сравнению с стандартным OpenGL.

Главное:
То что видно в GLSurface имеет координаты от -1 до +1 по X и Y осям.
Вне зависимости от реальных пропорций экрана.

Соответственно,
вычислить местоположения пиксела на экране можно таким способом:
𝑋𝑃 𝑖𝑥𝑒𝑙𝑆𝑖𝑧𝑒 = 2.0𝑓 /𝑋𝑆𝑐𝑟𝑒𝑒𝑛;
𝑌 𝑃 𝑖𝑥𝑒𝑙𝑆𝑖𝑧𝑒 = 2.0𝑓 /𝑌 𝑆𝑐𝑟𝑒𝑒𝑛;
Где 𝑋𝑆𝑐𝑟𝑒𝑒𝑛 и 𝑌 𝑆𝑐𝑟𝑒𝑒𝑛 – количество пикселов по оси на экране.

Соответственно,
перевести экранные (150, 100) в систему координат OpenGL:
𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛 = 𝑋𝑃 𝑖𝑥𝑒𝑙𝑆𝑖𝑧𝑒 * 150 − 1.0𝑓 ;
𝑌 𝑝𝑜𝑠𝑖𝑡𝑖𝑜𝑛 = 𝑌 𝑃 𝑖𝑥𝑒𝑙𝑆𝑖𝑧𝑒 * (𝑌 𝑆𝑐𝑟𝑒𝑒𝑛 − 100) − 1.0𝑓 ;

Текстуры имеют координаты от 0 до одного.


Про это подробней в следующем уроке.

Замечу, что в OpenGL ES 2.0 нет матрицы трансформации, камеры, мира – если мы сами
не зададим их и они нам нужны.
Соответственно нет установки проекций ( перспективной, ортогональной ) так как если мы
сами не преобразуем координаты в соответствии с перспективной, то используем ортогональ-
ную проекцию, и никто за нас не будет делать эти преобразования.

Вот и весь необходимый минимум знаний.


7 ТЕКСТУРЫ В OPENGL ES 2.0 37

7 Текстуры в OpenGL ES 2.0


Текстура в OpenGL ES 2.0 ( GLES20 ) – растровое изображение.
Текстуры обычно используются для наложения на полигоны как способ повышении реали-
стичности изображения.
По сравнению с ранними реализациями в GLES20 способы использования текстур намного
вышли за рамки простого наложения на полигоны.

Текстуры в GLES20 бывают двух видов:

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.

Важно:

Текстуры TEXTURE_1D в стандарте GLES20 нет.


Ее можно заменить 2D текстурой толщиной в 1 тексель. (тексель - пиксель текстуры.)

Буду описывать 2D текстуры, поэтому не буду уточнять, что они 2D.

Текстуры делятся по количеству каналов.


GLES20 поддерживает следующие виды:

ALPHA – один альфаканал(A)


RGB – три канала, Красный(R),Зеленый(G),Синий(B)
RGBA – четыре канала, Красный(R),Зеленый(G),Синий(B) и Альфа(A)
LUMINANCE – один канал, канал яркости(L) в шейдере будет задействован на канале R
LUMINANCE_ALPHA – два канала, канал яркости(L) и альфа(A), в шейдере будет задей-
ствован на каналах RA.
7 ТЕКСТУРЫ В OPENGL ES 2.0 39

Табличка:

Текстура LUMINANCE для программиста отличается от ALPHA только тем, на каком кана-
ле при выборке будут лежать данные.
При выборке (чтении данных с текстуры) в шейдере мы ВСЕГДА получаем четырехмерный
вектор V(r,g,b,a) и, в зависимости от типа текстуры, значения будут на определенном канале
( если канал не занят – значения будет 0.0 ).

Что представляет себя текстура для программиста?


Текстура – набор данных ( в принципе любых ) записанных в определенной последователь-
ности.

Основные свойства 2D текстуры:


* Высота и ширина
* Количество и вид каналов
* Бит на каждый канал

Например, 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

UNSIGNED_SHORT_5_6_5 – для шестнадцатибитных RGB текстур формата R5,G6,B5.

UNSIGNED_SHORT_4_4_4_4 – для шестнадцатибитных RGBA текстур формата R4,G4,B4,A4.

UNSIGNED_SHORT_5_5_5_1 – для шестнадцатибитных RGBA текстур формата R5,G5,B5,A1.

Важно:
Я описал не все возможные форматы – это форматы гарантированно поддерживающиеся
всеми GPU по стандарту OpenGLES 2.0.
Есть еще много Vendor-зависимых форматов, как то например R10G10B10A2 т.д.
Но их лучше никогда не использовать если конечно вы хотите чтобы кто-то с другим GPU
смог запустить приложение.
7 ТЕКСТУРЫ В OPENGL ES 2.0 40

7.1 NPOT и POT текстуры


POT текстура – текстура, стороны ( высота и ширина ) которой равны одной из степеней
двойки. Например 64,128,256,1024,2048 и т.д. ( POT – Power Of Two, то есть степень двой-
ки. )

NPOT – текстура с произвольным размером сторон, например 1000 или 58.


( NPOT – Non Power Of Two, то есть не степень двойки. )

OpenGL ES 2.0 гарантированно имеет частичную поддержку NPOT текстур.

Что это значит? И почему частичную?

Он гарантированно поддерживает работу с NPOT текстурами но не поддерживает тай-


линг и автоматическое построение mipmaps по NPOT-текстурам.

Конечно ваш GPU может поддерживать полную работу с NPOT-текстурами, но так как
это не входит в стандарт нельзя гарантировать, что это будет работать везде.

Так что если хотите использовать тайлинг или автоматическую генерацию мипмапов, то
придется использовать POT-текстуры.
Увы.

В данном случае под тайлингом понимается многократное повторение одной текстуры.

В OpenGL ES 2.0 нет поддержки зеркального тайлинга, когда текстура переворачивается


при четных ( или нечетных ) повторениях.

Вес ( Размер занимаемой памяти ) текстуры можно определить таким способом:


байт_на_пиксель*высоту*ширину.

То есть 32битная текстура размера 1024 на 1024 занимает 4*1024*1024 = 4’194’304 байт.
4 мегабайта!
Вот куда уходит большая часть памяти в играх.

16-битная текстура 1024 на 1024 займет уже только 2мб, так что стоит подумать – стоит
ли использовать 32битные или нет.

Есть еще аппаратное сжатие текстур, которое обычно позволяет до четырех раз умень-
шить вес текстур, об этом ниже.

7.2 Теперь касательно памяти в Android


Памяти в версиях системы ниже <3.0 на приложение доступно 16 или 24 или 32мб, в
зависимости от системы, из которых, следует учесть, 4-7мб занимает интерпретатор java, и
служебные данные, и сам код приложения.
При попытке загрузить что-то выше этого предела приложение упадет с сообщением пере-
полнения кучи.
А еще же есть звуки, музыка, игровые данные и т.д.
Получить максимальный размер кучи можно с помощью Runtime.getRuntime().maxMemory().
7 ТЕКСТУРЫ В OPENGL ES 2.0 41

То есть даже если у вашего аппарата два Гб. оперативной памяти то одному приложению
будет доступно все равно не больше, чем размер кучи.

Но это нам не помешает, так как все текстуры ( и не только текстуры ) после загрузки
хранятся в памяти драйвера, на которую не действуют эти ограничения.
Ограничение конечно есть, но это обычно 256мб для старых систем и 1гб+ для новых.
Всё это из-за того что OpenGL ES 2.0 работает в режиме клиент-сервер ( как уже писалось
выше ), в данном случае клиент – это видеодрайвер, а сервер наше приложение.
Но, теоретически, ничего не мешает клиенту и серверу находится даже на разных машинах.
Понятия Видеопамяти в андроид как такового нет, так как GPU запрашивает нужную у
системы из общей памяти и, соответственно, затруднительно посмотреть сколько осталось
свободной.

Проверить количество видеопамяти на вашем аппарате можно таким способом:


Создаем текстуры известного нам размера пока не поймаем ошибку GL_OUT_OF_MEMORY,
потом прибавляем к нашему числу размер экранного буфера.
Так же можно проверить доступность необходимого количества памяти, например, если мы
не сразу подгружаем все ресурсы, но знаем, что в пике нашему приложению нужно, напри-
мер, 82мб, для того чтобы приложение не свалилось посередине уровня.
Этот способ имеет еще один плюс – выделение памяти системой очень тяжелая опера-
ция и иногда лучше забить память сразу пустышками, а потом заменять их на нормальные
текстуры, так как видеодрайвер обычно не сразу высвобождает неиспользуемую память, да
и система нормально прихлопнет "ненужные"приложения для освобождения ресурсов что
обычно тоже далеко не мгновенный процесс.

Важно:
Так как клиент OpenGL не является частью нашей программы мы часто можем не узнать о
том, что операция закончилась некорректно.
Узнать об ошибке ( или наоборот хорошо закончившейся операцией ) с помощью функции
glGetError().
После каждой критической операцией, например такой как загрузка текстур, проверять зна-
чение возвращаемое glGetError() и, если оно не равно GL_NO_ERROR, то обрабатывать
ошибки.

В примерах я стараюсь поменьше использовать проверки, что-бы не загромождать код, но


в реальных задачах это НЕОБХОДИМО – иначе никогда не узнаете почему и из-за чего у
пользователя криво или некорректно работает.
Например не скомпилился шейшер – вместо меша разнообразные глюки, не загрузилась тек-
стура – черные квадратики, но определить по этим признакам конкретное место ошибки не
всегда представляется возможным.

Порядок загрузки ресурсов приложения лучше делать такой:

* Данные OpenGL - текстуры, статические массивы и всё то,


что будет хранится в памяти GPU.
* Почистить хвосты.
* Данные OpenGL которые будут хранится в памяти самого приложения, звуки, игровые
данные.

В другом порядке может уже не хватить памяти для загрузки OpenGL ресурсов.
И лучше это делать в один поток, понятно по какой причине...
7 ТЕКСТУРЫ В OPENGL ES 2.0 42

7.3 Максимальный размер текстуры


Минимальный размер текстуры определенный стандартом OpenGL ES 2.0 – 1024.
То есть все GPU, как минимум, поддерживают текстуры размера 1024x1024.

Максимальный размер поддерживаемых текстур можно узнать c помощью функции glGet*()

int[] max = new int[1];


GLES20.glGetIntegerv(GL10.GL_MAX_TEXTURE_SIZE, max, 0);

Как видно из названия glGetIntegerv возвращает целое число в max[0], которое соответству-
ет максимальному размеру поддерживаемой текстуры.

Но всё же, без надобности лучше не применять текстуры максимального размера доступ-
ного на вашем GPU, так как не факт, что на других устройствах будет поддержка текстур
размером больше 1024.

7.4 Текстурные координаты


Текстурные координаты в GLES20 задаются для каждой вершины вектором V(t,s).
t – высота, s – ширина.

Обозначения t и s судя по всему приняты для того чтобы не путать с другими данными.

Каждой вершине могут соответствовать несколько текстурных координат.


Количество текстурных координат на вершину в GLES20 не ограниченно.
7 ТЕКСТУРЫ В OPENGL ES 2.0 43

Текстурные координаты могут быть как меньше размера текстуры ( 1.0 по каждой оси )
так и больше.
Если координаты меньше – текстура увеличивается, больше – текстура уменьшается.
Геометрия самого полигона не влияет на текстурные координаты.

Про то, что бывает, когда текстура меньше текстурных координат, чуть ниже.

7.5 Текстурные слоты (Texture Units)


Важное понятие OpenGL - Текстурные слоты (Texture Units).

Количество одновременно загруженных текстур ничем не ограниченно – тогда как ядро


GPU может работать только с определенным количеством текстур одновременно.

В стандарте GLES определено как минимум 8 слотов для текстур фрагментного шейдера
и 0 ( ! ) текстур для вершинного.

Запросить количество поддерживаемых текстурных слотов можно с помощью функции


glGet* с параметрами:

MAX_VERTEX_TEXTURE_IMAGE_UNITS – слотов для вершинного шейдера.

GL_MAX_TEXTURE_IMAGE_UNITS – слотов для фрагментного шейдера.

GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS – сколько всего можно одновременно за-


действовать слотов в сумме ( вершинных и фрагментных ).

Замечу, что GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS необязательно соответ-


ствует сумме MAX_VERTEX_TEXTURE_IMAGE_UNITS и GL_MAX_TEXTURE_IMAGE_UNITS.
Вполне возможен вариант когда например и вершинных 8, и фрагментных 8, а всего поддер-
живается тоже 8.

Слоты для вершинного шейдера не поддерживает ни Tegra, ни Mali, поэтому для универ-
сальности придется отказаться от этой технологии =(. Но в защиту их будет сказано, что
они имеют другой функционал, который позволяет реализовать похожие эффекты.
7 ТЕКСТУРЫ В OPENGL ES 2.0 44

Выбрать текущий слот для работы ( подключению к нему текстуры, изменение параметров
текстуры ) можно с помощью процедуры:
GLES20.glActiveTexture(GLES20.GL_TEXTUREx);

где GLES20.GL_TEXTUREx – номер выбранного слота, например GLES20.GL_TEXTURE0,


константы прописаны для 32 текстур ( последняя GL_TEXTURE31 ).

По умолчанию выбран слот GL_TEXTURE0.

Для удобства:
константы начиная с GL_TEXTURE0 идут по порядку,
так что GL_TEXTURE3 = GL_TEXTURE0+3;

Для подключения текстуры к слоту используется процедура


GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture_id);

Где:
первый параметр – тип текстуры,
второй – ссылка на текстуру.

Эта процедура прикрепляет текстуру к !текущему! слоту, который был выбран до этого
процедурой GLES20.glActiveTexture().

То есть для того чтобы прикрепить текстуру к определенному слоту нужно вызвать две
процедуры:
GLES20.glActiveTexture(Номер_Слота);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, ссылка_на_текстуру);

Пример подключения текстур:

Важно:
Нельзя подключить одну текстуру одновременно к нескольким слотам.
Если вы ее переключили на другой слот и не установили текстуру для слота, куда она была
подключена раньше, попытаетесь из него прочесть – результат вплоть до падения драйвера.

Напомню, тексел – пиксел текстуры.


7 ТЕКСТУРЫ В OPENGL ES 2.0 45

7.6 Параметры семплирования текстур в OpenGL ES 2.0


Параметры семплирования устанавливаются для текстуры текущего выбранного текстур-
ного слота.
Если попытаться установить параметры для слота к которому не закреплена текстура то
получим ошибку.

Для текстур в GLES20 можно задать только четыре параметра:


GL_TEXTURE_MIN_FILTER
– фильтр текстур при уменьшении размера текстуры от исходного
GL_TEXTURE_MAG_FILTER – фильтр при увеличении размера от исходного
GL_TEXTURE_WRAP_S
– поведение при текстурных координатах больших размера текстуры по ширине.
GL_TEXTURE_WRAP_T
– поведение при текстурных координатах больших размера текстуры по высоте.

GL_TEXTURE_MIN_FILTER и GL_TEXTURE_MAG_FILTER могут принимать значения:


GL_NEAREST – нет фильтрации, нет мипмапов
GL_LINEAR – фильтрация, нет мипмапов
GL_NEAREST_MIPMAP_NEAREST – нет фильтрации, выбор ближайшего уровня мипмап
GL_NEAREST_MIPMAP_LINEAR – нет фильтрации, фильтрация между уровнями мипмап
GL_LINEAR_MIPMAP_NEAREST – фильтрация, выбор ближайшего уровня мипмап
GL_LINEAR_MIPMAP_LINEAR – filtering, фильтрация между уровнями мипмап

Названия фильтраций:
GL_LINEAR – билинейная
GL_LINEAR_MIPMAP_NEAREST – билинейная с мипмапами
GL_LINEAR_MIPMAP_LINEAR – трилинейная

Трилинейная фильтрации дает лучший визуальный результат, но и более требовательна


к ресурсам.
Например для 2D графики она не нужна, тут достаточно максимум билинейной с мипмапами.

Режимы с мипмапингом имеют смысл только для GL_TEXTURE_MIN_FILTER.

Мипмапинг ( MipMaps ):
ссылочка на описание

Скажу только что это улучшает качество фильтрации но увеличивает расход памяти. На
скорость может влиять как положительно так и отрицательно в зависимости от GPU.

Автоматически создать мипмапы ПОСЛЕ загрузки текстуры можно с помощью процедуры


GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
7 ТЕКСТУРЫ В OPENGL ES 2.0 46

GL_TEXTURE_WRAP_S,T могут принимать значение:

Пример наложения текстуры( два полигона образующих прямоугольник ), текстурные


координаты от 0 до 1:

GL_CLAMP_TO_EDGE – если текстурные координаты выходят за границу крайние тек-


селы текстуры подтягиваются к границе полигона.

Пример, текстурные координаты умноженные на 2, режим GL_CLAMP_TO_EDGE:


7 ТЕКСТУРЫ В OPENGL ES 2.0 47

GL_REPEAT – изображение будет повторятся при выходе за размер текстуры.

Пример, текстурные координаты умноженные на 2, режим GL_REPEAT:

Еще режим GL_REPEAT, когда одна текстура повторяется несколько раз называют тай-
лингом.

Это все доступные параметры семплирования текстур в OpenGL ES 2.0.


То есть нет режимов смешивания и т.д. из прошлых версий API.

Текстурные параметры можно изменить в любой момент времени, а не только при ини-
циализации.

По умолчанию параметры установлены в GL_REPEAT и GL_LINEAR.


7 ТЕКСТУРЫ В OPENGL ES 2.0 48

7.7 Хранение текстур в памяти

Texture Storage – данные текстуры, сам массив значений текселов.


Sampling Parameters - параметры семплирования текстуры
Texture Parameters – параметры текстуры, такие как высота, ширина, формат и т.д.

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

7.8 Загрузка текстуры

Листинг 8: Загрузка текстуры


texture_id = new int[1]; // массив ссылок на текстуру, в данном случае массив для о
дной текстуры.

GLES20.glGenTextures(1, texture_id, 0);


//первый параметр – количество генерируемых ссылок,
//второй – массив для возврата результата,
//третий – с какого элемента с какого элемента массива начинать запись.

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture_id[0]);
// прикрепляем текстуру к текущему слоту. Первый параметр – тип текстуры, второй –
ссылка на текстуру.
// Внимание: помните какой сейчас текущий текстурный слот

// Пускай Bitmap bm = Load_Bitmap(name);

int format = GLUtils.getInternalFormat(bm);


// получаем формат текстуры

int type = GLUtils.getType(bm);


// получаем тип текстуры
7 ТЕКСТУРЫ В OPENGL ES 2.0 49

GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, format, bm, type, 0);


// загружаем текстуру в память GPU
// второй параметр – уровень мипмапа, служит для ручной загрузки уровней мипмапа.
// Последний параметр – Border ( рамка ). В OpenGL ES 2.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.

// для генерации мипмапов нужно вызвать GLES20.glGenerateMipmap(GLES20.


GL_TEXTURE_2D);
// и задать соответствующий режим фильтрации в GLES20.GL_TEXTURE_MIN_FILTER
// Для гарантированной поддержки тайлинга и мипмапинга нужно использовать POT текст
уры

Как видите для загрузки я использовал GLUtils.texImage2D вместо GLES20.glTexImage2D.

GLUtils - библиотечка для вспомогательных функций, обычно реализация под конкрет-


ную систему.

Так как GLES20.glTexImage2D не умеет работать с Bitmap и понимает только массив


данных текстуры, то нам самим пришлось бы получать данные из Bitmap ( или загружать
текстуру в виде данных и декодировать ее ).

В библиотеке 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

Листинг 9: Загрузка текстуры (без комментариев)


texture_id = new int[1];
GLES20.glGenTextures(1, texture_id, 0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture_id[0]);

int format = GLUtils.getInternalFormat(bm);


int type = GLUtils.getType(bm);
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, format, bm, type, 0);
bm.recycle();

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);

7.9 Удаление текстур


C помощью процедуры glDeleteTextures.

Листинг 10: Удаление текстуры


GLES20.glDeleteTextures(1, texture_id, 0);
// первый параметр – количество генерируемых ссылок, второй – массив для возврата р
езультата,
//третий – с какого элемента массива удалять.

7.10 Сжатие текстур


Для уменьшения объема текстур используется сжатие, что позволяет в 3-5 раз умень-
шить занимаемый объем памяти.

OpenGL ES 2.0 НИКАК не регламентирует поддерживаемые форматы - но 100% ваш GPU


поддерживает какие-либо.

Часть из них:

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

Vivante Corporation GC800+ core:


GL_EXT_texture_compression_dxt1
GL_EXT_texture_compression_s3tc
...
GL_OES_compressed_ETC1_RGB8_texture

и т.д.

Пример сжатия:

Замечательная статья с рассмотрением основных видов сжатия, их достоинств и недо-


статков под Android. Хотя-бы просмотреть обязательно.

Поддержка этих форматов является расширениями OpenGL

7.10.1 Система расширений


Система расширений позволяет производителям добавлять в библиотеку функциональ-
ность через механизм расширений.
Каждый производитель имеет аббревиатуру, которая используется при именовании его новых
функций и констант.
Например, компания NVIDIA имеет аббревиатуру NV, Qualcomm QCOM, AMD -AMD и т.д.
7 ТЕКСТУРЫ В OPENGL ES 2.0 52

Может случиться так, что определённое расширение могут реализовать несколько произ-
водителей.
В этом случае используется аббревиатура EXT.
В случае же, когда расширение одобряется консорциумом, оно приобретает аббревиатуру
ARB и становится стандартным расширением.
Обычно расширения, одобренные консорциумом, включаются в одну из следующих специ-
фикаций OpenGL.

Получить список всех поддерживаемых расширений вашего GPU можно функцией glGetString.

Список всех поддерживаемых расширений


String Extensions = GLES20.glGetString(GLES20.GL_EXTENSIONS);

И потом в этой строчке искать нужное нам расширение.


Если не находится – значит наш GPU его не поддерживает.

Для использования части расширений придется делать прямой импорт функций из биб-
лиотек драйверов, что возможно только под NDK,
Так что, если хотите использовать нестандартные функции – придется писать свою библио-
течку враппера на С++.

Некоторые расширения только указывают на какие-то возможности и не имеют дополни-


тельных функций для работы с ними, например, расширение
GL_OES_texture_npot указывает на то, что GPU поддерживает тайлинг и автоматическое
построение мипмап-уровней для NPOT-текстур (стороны не равны степени двойки).

7.10.2 Загрузка сжатых текстур


Для загрузки сжатых текстур используется функция GLES20.glCompressedTexImage2D
вместо GLES20.glTexImage2D();
CompressedTexImage2D(enum target, int level,enum internalformat, sizei width, sizei height,int
border, sizei imageSize, void *data);
target – вид текстуры, например TEXTURE_2D
level – уровень мипмапкарты
internalformat – формат сжатия.

Хотя некоторые GPU умеют на лету сжимать текстуры – обычно это очень тяжелая опе-
рация и текстуры подгружаются уже сжатыми.
Существуют как отдельные утилиты, так и плагины для редакторов для работы со сжатыми
текстурами.
Скачать можно с сайтов производителей чипов.

Сжатые текстуры – это почти всегда главная причина разного кеша приложений под раз-
ные устройства.
Так что, если кто ковыряет кеши и не знает форматы – текстуры 99% хранятся в каком либо
формате производителя конкретного GPU.

Вот и все про 2D текстуры.


8 ШЕЙДЕРЫ 53

8 Шейдеры
8.1 Немножко истории
Первоначально почти каждый разработчик писал свою версию рендера, как в случае с
Doom, Quake1, Duke nukem 3D и других.
Разработчик мог полностью управлять всеми аспектами рендеринга и в общем-то не было
никаких ограничений кроме производительности системы ( и рук).

Но, так как CPU тех времен достаточно плохо справлялись с рендеренгом в реальном
времени ( да и сегодняшние тоже не очень, так как специализированные процессоры за-
точенные под определенные задачи всегда быстрее универсальных ), а некоторые нужные
вещи как сглаживание текстур вообще превращало приложение в слайд-шоу, возникла по-
требность в отдельном аппаратном блоке разработанном именно под определенные расчеты.

Первым успешным ”игровым” видеоускорителем можно считать Voodoo 3dfx вышедшей в


1996 году.
У них был слоган: ”30 кадров в секунду любой ценой” =).
С развитием видеоускорителей, начиная с Voodoo который был первый ”игровой” GPU для
настольных компьютеров, так как профессиональные для рабочих станций появились рань-
ше – но стоили как чугунный мост, например SGI.
Как раз на это время приходится начало ”бума” 3D графики, что является главным двигате-
лем прогресса в производительности компьютеров и по сей день.
На мой взгляд если бы не компьютерные игры, к коим многие относятся несерьезно, то
компьютерная индустрия не получила бы такого развития на нынешний момент. . .

Но, на самом деле, была ещё и обратная сторона переноса любых расчетов на GPU.

И началась в ”игровых” GPU именно с первой Voodoo.


Да, текстуры выглядели сглаженными, скорость расчетов была на несколько порядков быст-
рее, чем на центральном процессоре – но программист уже не мог влиять на процесс ренде-
ринга и выйти за рамки API и аппаратных функций видеоускорителя!

Вспомните первый Quake, когда там падаешь в воду, изображение искривлялось, или
CounterStrike где на софтварном движке при взрыве гранаты была не просто белая вспышка
– а пикселизация изображения, или Elite 3 с ее процедурными планетами – на которые
реально было посадить корабль и т.д.
8 ШЕЙДЕРЫ 54

Например, как вы видите, искривления при падении в воду в Q1.

Разработчики стали очень тесно зажаты в рамки API, будь то Glide, DirectX или OpenGL
(не путать OpenGL с OpenGLES).

Есть два способа освещения – ими и пользуйтесь, ничего изменить нельзя.


Есть максимум восемь источников освещения – девятый не добавите.

И сами API были далеки от совершенства. . .

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

Не было возможности сделать постобработку изображения.

В первый раз в играх слово шейдер появилось в Quake3 в виде языка смешивания текстур
и аппаратной поддержкой в GPU Nvidia TnT и выше ( и аналогичных Radeon от ATI и S3 ).

Но все равно гибкость мультитекстурирования была недостаточная.


Разработчики могли работать на уровне примитивов, но никак не отдельных пикселей, не
было возможностей пост-обработки изображения и аппаратного просчета изменения геомет-
рии.

Видеоускорители продолжали развиваться, становясь быстрее и дешевле, появлялись но-


вые фишки, как например T&L ( перенос преобразований и освещения на GPU ).
С приходом GeForce 256 появилась технология рельефного текстурирования...
Что позволило уже сделать не повершинное освещения а ”попиксельное”.
Но гибкость решения все равно была недостаточная.
8 ШЕЙДЕРЫ 55

Вспомните игры тех времен, например первый BloodLine, где все залито бамп-маппингом.

Разработчики оказались ещё сильнее ограничены аппаратно реализованными функциями.

Да ещё в добавок GPU от разных производителей поддерживали разные, порой вообще


не совместимые между собой возможности.

Все изменилось с выходом GeForce3 и DirectX8.

Появились первые полностью программируемые шейдеры.


Именно из-за них стали возможны многие эффекты, такие как пост-обработка, возможность
программного изменения буферов вершин ( например аппаратная скелетная анимация, DX7
не в счет ) и настоящее ”попиксельное” освещение, что позволяло создавать по тем временам
удивительные и красивые эффекты которые мы видели в Doom3, Morrowind, Newerwinter
Nights.

GPU того поколения были ещё с очень сильными ограничениями, но это уже был огром-
ный скачек к переходу от фиксированной логики к программируемой.

В те времена шейдеры ещё писались на ассемблероподобном языке.


И опять была проблема сегментации из-за разных производителей GPU.

Да ещё Microsoft в те времена пыталась угробить OpenGL любыми способами, зачем


им хотелось владеть единственным API для трехмерной компьютерной графики, я думаю,
уточнять не надо, спасибо NVidia, что этого не случилось ( кому интересно прочитайте про
проект Fahrenheit, с которым она тормозила развитие OpenGL на протяжении пяти лет ).

Первой язык высокого уровня для написания шейдеров Cg создала NVidia.


Естественно, он работал только на продуктах от компании.

Microsoft создала высокоуровневую надстройку HLSL только в DirectX9, да и то разра-


ботчикам зачастую ( да и сейчас иногда ) приходилось писать несколько вариантов шейдеров,
для ATI и NVidia в отдельности.

Но вернемся к OpenGL во времена выхода DirectX8.

Тут, как раз, начинается бурное развитие стандарта.


Многие расширения становятся частью стандарта, появляется возможность использовать
программируемую логику.

На нынешнее время стандарт OpenGL если не обгоняет DirectX, то точно не отстает, так
как поддержка новых возможностей за счет расширений появляется куда быстрее чем новая
версия DirectX.

Но у OpenGL, как и у DirectX, есть одно очень нехорошее свойство – старость.

У DirectX это выражается в очень плохой обратной совместимостью, так как версии
DX3D зачастую не совместимы между собой даже на уровне логики построения приложения,
перейти с DX7 на DX11 представляется мало возможным, так как придется переделывать
весь рендер ( а часто и все остальное ”блоки” ) с ”нуля”.
8 ШЕЙДЕРЫ 56

С настольным OpenGL ситуация обратна. Вы с минимум изменений можете пересобрать


приложение разработанное например для OpenGL 1.3 под OpenGL 4.3 и спокойно использо-
вать новые возможности API при минимально-необходимой переделке кода.
Но за это плата – ”разбухание” стандарта. Многие функции и технологии устарели настоль-
ко что их использование не только не актуально, но и вредно.

Например вызовы glVertex3f, glColor3f, glNormal3f и glTexCoord2f.

Для примера:
Пускай у нас есть 3D меш на 10к полигонов ( отдельных ),
дополнительные атрибуты вершин: цвет, нормаль и текстурные координаты.

Для отрисовки старым способом нам бы понадобилось 10.000*3*4 = 120.000 вызовов


функций. Современным способом это можно сделать за десять-пятнадцать вызовов.

Но спросите почему до сих пор не откажутся от таких явно устаревших и даже вредных
функций в 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, а некоторые возможности вообще уникальны ).

И я не удивлюсь если OpenGL ES в дальнейшем будет более распространенным стандар-


том на настольных системах чем ”полный” OpenGL.
К этому все и ведет, ведь например WebGL является ничем другим как OpenGLES 2.0

На нынешний момент последняя спецификация OpenGLES версии 3.0.


Но, так как в живую её пощупать не удастся, пока что описывать её не буду.

OpenGLES 3.0 обратно совместим с OpenGLES 2.0, ваши программы разработанные под
2.0 будут себя прекрасно чувствовать и на 3.0

Язык для программирования логики работы GPU в OpenGL ES 2.0 – GLSL version 1.0.0
8 ШЕЙДЕРЫ 57

8.2 Шейдеры в OpenGL ES 2.0 и язык GLSL.


Шейдер - это программа для выполнения определенной задачи на GPU.
В силу специфики компьютерной графики и внутреннего устройства GPU используется спе-
циализированные языки для описания программ.
Для OpenGL ES 2.0 таким языком является GLSL.

В OpenGL ES 2.0 используются два вида шейдеров:

Вершинный шейдер
Выполняет обработку геометрии, то есть изменяет параметры вершины, такие, как пози-
цию, текстурные координаты, цвет вершин.
Также может выполнять вычисления освещения. Выполняется для каждой вершины (!).

Фрагментный шейдер
Выполняет обработку цветовых данных, полученных при рисовании треугольника.
Оперирует с текстурами и цветом.
Количество инструкций значительно ограничено производительностью. Выполняется для
каждого пикселя (!).

Вершинный и фрагментный шейдер в OpenGL ES 2.0 ВСЕГДА РАБОТАЮТ В ПАРЕ.

Связку вершинного и фрагментного шейдера называют ШЕЙДЕРНОЙ ПРОГРАММОЙ


или просто ПРОГРАММОЙ.

Для описания шейдеров используется язык GLSL (OpenGL Shading Language).

8.2.1 Описание GLSL


Синтаксис GLSL базируется на языке программирования ANSI C, однако, из-за его спе-
цифической направленности, из него были исключены многие вещи, но и было добавлено
много особенностей.
Как и Си, язык GLSL чувствителен к регистру.

8.2.2 Типы данных GLSL


Скалярные
float
int
bool

Вектора с плавающей запятой

float – одномерный вектор


vec2 – двухмерный вектор
vec3 – трехмерный вектор
vec4 – четырехмерный вектор
8 ШЕЙДЕРЫ 58

Целочисленные вектора

int – одномерный вектор


ivec2 – двухмерный вектор
ivec3 – трехмерный вектор
ivec4 – четырехмерный вектор

Boolean-вектора ( разрядность по аналогии )

bool
bvec2
bvec3
bvec4

Матрицы

mat2 – матрица 2x2


mat3 – матрица 3x3
mat4 – матрица 4x4

Пример
float u_Time; // скаляр с плавающей запятой
vec4 a_Position; // четырехмерный вектор с плавающей запятой
mat4 m_Projection; // матрица 4x4
ivec2 г_Offset; // целочисленный двухмерный вектор

Инициализацию переменных можно проводить вовремя или после объявления перемен-


ных.

Целочисленные значения можно использовать только в константах!

8.2.3 Конструкторы
Конструкторы применяются для инициализации данных или преобразования типа.

Преобразование типа можно проводить только с типами данных одинаковой разрядности


( ну, я думаю, тут понятно, как к скаляру трёхмерный вектор приведёшь? ).

Синтаксис я думаю будет понятен по примерам.

Конструкторы данных
bool myBool = true; // объявление и инициализация boolean-переменной.
float myFloat;

myFloat = float(myBool); // преобразование Boolean к float


myBool = bool(myFloat); // преобразование float к Boolean

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 // третий столбец
);

8.2.4 Компоненты матриц и векторов


К компонентам матриц и векторов можно обращаться как к обычным массивам. Но есть
способ удобней. Обращаться можно через ”.” с помощью специальных наборов символов x,
y, z, w, r, g, b, a или s, t, r, q.

Компоненты
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

vec2 myVeci = myVec.xy // myVec2 = {0.0,1.0}


myVeci = myVec.xx // myVec2 = {0.0,0.0}
myVeci = myVec.yy // myVec2 = {1.0,1.0}

vec3 myVec3 = vec3(0.0, 1.0, 2.0); // myVec3 = {0.0, 1.0, 2.0}


vec3 temp;
temp = myVec3.xyz; // temp = {0.0, 1.0, 2.0}
temp = myVec3.xxx; // temp = {0.0, 0.0, 0.0}
temp = myVec3.zyx; // temp = {2.0, 1.0, 0.0}

vec4 myVec4 = vec4(0.0,0.3,0.7,1.0);


float f = myVec4.z // f = 0.7
float f2 = myVec4.a // f2 = 1.0
vec2 posVec = myVec4.aa // posVec = {1.0,1.0}
posVec = myVec4.zx // posVec = {0.7,0.0}
posVec = myVec4.ax // ошибка! Нельзя смешивать разные наборы символов!

Как видите из названий x, y, z, w удобней применять для координат, r, g, b, a для


значения цвета, а s, t, r, q я не люблю и в примерах заменяю на координаты ( а вообще это
для текстур, символы rq вообще от балды – наверное все хорошее уже заняли ).
8 ШЕЙДЕРЫ 60

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;

LightVar = LightStruct(vec4(0.5, 1.0, 0.0, 0.0), // position


vec4(0.0, 1.0, 1.0, 0.0), // color
0.5); // force

vec4 pos = fogVar.position;


float f = fogVar.force;

8.2.7 Массивы

Массивы
float floatArray[4]; // массив из четырех скаляров
LightStruct vecArray[8]; // массив из восьми структур LightStruct

Индексация массивов начинается с 0.


8 ШЕЙДЕРЫ 61

8.2.8 Операнты
* Умножение
/ Деление
+ Сложение
− вычитание
++ Инкремент (пре или пост)
−− декремент (пре или пост)
= Присваивание
+=, —=, *=, /= арифметическое присваивание
==, !=, <, >, <=, >= Сравнение
&& логическое и
^^ Логическое исключающее или
|| Логическое или

Важно:
На некоторых GPU операции * и *= быстрее, чем / и /=

Поэтому вместо деления используйте умножение на дробь.


float x = x/2.0; // медленнее на некоторых GPU
float y = y*0.5; // быстрее

8.2.9 Функции

Пример
vec4 diffuse(
vec3 normal,
vec3 light,
vec4 baseColor)
{
return baseColor * dot(normal, light);
}

Важно:
Функции в GLSL не могут быть рекурсивными.

8.2.10 Операторы ветвления и циклы


Синтаксис в точности как у Си ( или Java )

Пример
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.
Старайтесь заменить ветвления на преобразования.
Или на два прохода.
Часто это быстрее, чем одно ветвление в фрагментной шейдере.

8.2.11 Прыжки и возвращаемые значения

Прыжки и возвращаемые значения


continue;
break;
return;
return выражение;
discard; // доступно только в фрагментном щейдере.

continue, break и return работают также как в Си ( или Java ).

Отдельно стоит сказать про discard.


discard доступен только в фрагментном шейдере.
Вызов discard вычеркивает пиксель, как будто у него не только альфа равна 0, но и не
учитывает его в буфере глубины и остальных буферах.
После вызова все вычисления по заданному пикселю прекращаются.
Discard не зависит от режима блендинга ( смешивания ).

И главное:
discard просто убивает производительность почти на всех адаптерах, является тяжелей-
шей операцией.
Производители рекомендуют не использовать discard, в крайнем случае в этом участке
(!) не должно быть прозрачных элементов, но в любом случае потери производительности
будут большими.
Внимательные спросят: Как нечего-неделанье может что-то тормозить? А дело в том что
при вызове discard чипу нужно остановить ВСЕ процессоры и перестроить буфер глубины,
8 ШЕЙДЕРЫ 63

stencil, перестроить фрагмент и т.д. и только после этого можно продолжать работать. Это
относится к RowerVR, adreno, Mali и другим GPU со схожей архитектурой. На Tegra влияние
слабее.
Кстати, плавное ”сгорание” трупов монстров в Doom3 реализовано как раз через discard
=)

8.2.12 Разрядность (точность) данных


Точность данных задается с помощью ключевых слов:
highp – высокая точность
mediump – средняя точность
lowp – низкая точность
Точность
lowp vec4 myVec; // четырехмерный вектор с низкой точностью.
highp float f; // float с высокой точностью
mediump float f2; // среднюю точность

Таблица диапазонов данных:

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}

Важно: Фрагментный шейдер может не поддерживать highp точность.


Проверить поддержку можно с помощью встроенной константы GL_FRAGMENT_PRECISION_HIGH

\begin{lstlisting}
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif

Нужно всегда стараться выбирать минимально-возможную точность для каждой перемен-


ной, так как это влияет на производительность.
8 ШЕЙДЕРЫ 64

Теперь о влиянии класса точности на производительность:

Блок USSE у чипов PowerVR может за один ”такт” сделать:


одно действие со скаляром highp или
одно действие с двумерным вектором mediump или
одно действие с трех-четырехмерным вектором lowp

Соответственно одна операция с четырехмерным вектором highp займет четыре такта


против одного с lowp.

У других чипов картина приблизительно такая же.

8.2.13 Специальные ”встроенные” переменные


В GLSL используется несколько встроенных переменных, как входящих так и исходящих.
Входящие условно можно считать параметрами main() функции шейдера, а исходящие воз-
вращаемым значением.
Исходящие специальные переменные необязательно должны быть ”заполнены” ( правда
обычно без них шейдер не имеет смысла ).

В вершином шейдере:

highp vec4 gl_Position


– ( исходящая ) переменная задающая трансформированное положение вершины в простран-
стве, доступна только в вершинном шейдере.
Используется для записи положения вершины после всех трансформаций, условно можно
считать ”возвращаемым значением” вершинной подпрограммы.
Единицы измерения – оконные координаты OpenGL, то есть видимый участок будет между
-1 до +1 по каждой оси.

mediump float gl_PointSize


– ( исходящая ) переменная, задает размер точки или точечного спрайта, доступна только в
вершинном шейдере.
”Возвращаемое” значение вершинного шейдера.
Имеет смысл только для точек и точечных спрайтов.
Единицы измерения – пикселы.

В фрагментном шейдере:

mediump vec4 gl_FragCoord


– ( входящая ) координаты фрагмента в координатах OpenGL, доступна только в фрагмент-
ном шейдере.
Содержит x, y, z, 1/w координаты фрагмента, которые являются интерполяцией переменных
gl_Position между тремя вершинами в данной точке.
Единицы измерения – оконные координаты OpenGL.
При мультисемплинге может принимать как значение исходящей точки, так и значения от-
дельных семплов в зависимости от GPU.

bool gl_FrontFacing
– ( входящая ) фрагмент находится на лицевой ( true ) или обратной стороне полигона.
8 ШЕЙДЕРЫ 65

mediump vec2 gl_PointCoord


– ( входящая ) координаты текстуры для "точки"( спрайта ).
Доступна только в фрагментном шейдере.
Единицы измерения – от 0 до 1 по каждой оси.

Важно:
Часть спецификаций ( и учебников ) описывает gl_PointCoord как mediump int gl_PointCoord,
что неверно.

mediump vec4 gl_FragColor


– ( исходящая ) возвращаемый цвет фрагмента из вершинного шейдера. Имеет формат
RGBA.

mediump vec4 gl_FragData


– ( исходящая ) массив исходящих значений.
Используется для рендеринга в несколько буферов одновременно.
Ну, например, мы хотим в дополнении к буферу цвета собирать данные геометрии в так
называемый G-буфер.
В остальном аналогично gl_FragColor.

И единственная специальная Uniform gl_DepthRange.


struct gl_DepthRangeParameters {
highp float near; // n
highp float far; // f
highp float diff; // f - n
};

uniform gl_DepthRangeParameters gl_DepthRange

Я думаю тут все понятно, gl_DepthRange хранит значение глубины, координаты экранные.
Если GPU не поддерживает highp во фрагментном шейдере, то точность будет mediump.

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

8.2.14 Специальные ( встроенные ) константы

Специальные константы
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;

Зависят от конкретного GPU и драйверов.


Имеют смысл для использования с препроцессором.
8 ШЕЙДЕРЫ 66

8.2.15 Коммуникация Android-приложения и шейдера


Для связи с нашей ( Android ) программой в GLSL используются специальные типы пе-
ременных:

1. Attribute (Вершинные атрибуты)


Используются ТОЛЬКО в вершинном шейдере.
Могут включать в себя такие данные как позицию вершины, нормали, текстурные коор-
динаты или любые другие данные, характерные для КОНКРЕТНОЙ ВЕРШИНЫ.

2. Uniforms (Униформы).
Используются как в вершинном так и фрагментной шейдерах.
Могут включать в себя любые данные ЕДИНЫЕ ДЛЯ ВСЕГО ОТРИСУЕМОГО МЕША.
Например время или положение и поворот всей модели, матрицы преобразований или
матрицу вида и т.д.

3. Varying ( Варинги =) )
Используются ТОЛЬКО для связи между вершинным и фрагментным шейдером.
Являются исходящими данными из вершинного шейдера.
На входе в фрагментный шейдер представляют из себя интерполированное значение меж-
ду вершинами примитива.

4. Sampler ( Текстура )
Могут использоваться как в фрагментном так и в вершинном.
Не все GPU поддерживают работу в текстурами в вершинном шейдере.

Важно:
Максимальное количество данных каждого типа имеет разное значение на разных GPU.

Стандартом определенны минимальные значения:


Attribs - 8
Uniform - 128
Varying – 8

Важно:
Все значения для четырехмерных векторов.

Важно:
Последовательность записи специальных типов переменных: "специальность" точность
имя.
varying lowp vec4 color; // правильно
uniform mediump float time; // правильно
mediump uniform float time; // ошибка
8 ШЕЙДЕРЫ 67

Связь данных и шейдера:

Для уменьшения задействованных переменных можно запаковывать несколько значений


в один вектор.
Например, не имеет смысла использовать два атрибута для хранения двух текстурных
координат, так как их можно запаковать в один четырехмерный вектор.

8.2.16 Invariant переменные


В моих примерах не используются, только скажу, что они нужны для получения одних
и тех же значений переменных в нескольких проходах. При их использовании ухудшается
оптимизация шейдера.

8.2.17 Встроенные функции


В GLSL встроенные функции можно разделить на три условные группы:

1. Функции реализующие аппаратные операции GPU, такие как доступ к текстурам.


Эти функции нельзя эмулировать другими средствами.

2. Функции реализующие простые операции такие как Mix, Clamp, Abs и т.д.
В принципе эти функции легко реализуются пользователем – но встроенные имеют в
основном аппаратную поддержку ( а часто являются отдельной операцией GPU ).
Но в любом случае производитель лучше знает свое ”железо”, так что шанс написать
лучше реализацию очень мал.

3. Функции всегда реализованные аппаратно для ускорения некоторых операций.


К таким относятся тригонометрические функции.
8 ШЕЙДЕРЫ 68

Часто функции названы по аналогии с Сишными библиотеками, но в силу специфики


могут принимать и возвращать такие типы как вектора и т.д.

Типы данных как float, vec2, vec3, или vec4 буду обозначать для краткости как genType,
матрицы mat2, mat3, или mat4 как mat.

8.2.18 Тригонометрические функции


Эти функции работают покомпонентно.
Описание для действия над каждым компонетном.

genType radians(genType Градусы) – переводит градусы в радианы ((Пи/180)*Градусы))


genType degrees(genType Радианы) – переводит радианы в градусы ((180/Пи)*Радианы))
genType sin (genType Угол) – синус угла ( в радианах )
genType cos (genType Угол) – косинус
genType tan (genType Угол) – тангенс
genType asin (genType x) – арксинус
genType acos (genType x) – арккосинус
genType atan (genType y, genType x) – арктангенс.
Возвращает угол, тангенс которого у / х.
Используется если нужно определить квадрант.
genType atan (genType x) ) – арктангенс

8.2.19 Экспоненциальные функции


Эти функции работают покомпонентно.
Описание для действия над каждым компонетном.

genType pow (genType x, genType y) – возвращает x в степени y.


Если x < 0 возвращает неопределенное значение.
Если x = 0 и y <= 0 возвращает неопределенное значение.
genType exp (genType x) – натуральная экспонента x
genType log (genType x) – натуральный логарифм x
genType exp2 (genType x) – возвращает 2 в степени x
genType log2 (genType x) – возвращает логарифм по основанию 2
genType sqrt (genType x) – квадратный корень
genType inversesqrt (genType x) – обратный квадратный корень, возвращает 1/sqrt(x)

8.2.20 Общие функции


Эти функции работают покомпонентно.
Описание для действия над каждым компонетном.

genType abs (genType x) – Возвращает x if x >= 0, в противном случае –x.


genType sign (genType x) – Возвращает 1.0 если x > 0, 0.0 если x = 0, или –1.0 if x < 0
genType floor (genType x) – возвращает ближайшее целое число меньше или равно x
genType ceil (genType x) – возвращает ближайшее целое число больше или равно x
genType fract (genType x) – возвращает x – floor (x)
genType mod (genType x, float y) – возвращает x – y * floor(x/y)
8 ШЕЙДЕРЫ 69

genType mod (genType x, genType y)


genType min (genType x, float y) – возвращает y если y < x, в противном случае x
genType min (genType x, genType y)
genType max (genType x, genType y) – возвращает y если y > x, в противном случае x
genType max (genType x, float y)
genType clamp (genType x, genType minVal, genType maxVal)
– возвращает min (max (x, minVal), maxVal)
genType clamp (genType x, float minVal, float maxVal)
Если minVal > maxVal результат не определен
genType mix (genType x, genType y, genType a)
– возвращает линейное спешивание x и y (x*(1-a)+y*a)
genType mix (genType x, genType y, float a)
genType step (genType edge, genType x)
– возвращает 0.0 if x < edge (край), в противном случае 1.0
genType step (float edge, genType x)
genType smoothstep (genType edge0, genType edge1, genType x) genType smoothstep (float
edge0, float edge1, genType x)
– возвращает 0.0, если х <= edge0 и 1.0, если х>= EDGE1
и выполнит интерполяцию Эрмита между 0 и 1 при edge0 < х < edge1.
Это полезно в тех случаях, когда нужна пороговая функция с гладким переходом.
Эквивалент
genType t;
t = clamp ((x – edge0) / (edge1 – edge0), 0, 1);
return t * t * (3 – 2 * t);

8.2.21 Геометрические функции


Действуют на векторы в качестве векторов, а не покомпонентно.

float length (genType x) – длина вектора x.


float distance (genType p0, genType p1) – дистанция между точками p0 и p1.
Эквивалент length (p0 – p1).
float dot (genType x, genType y) – скалярное произведение, x[0]*y[0]+x[1]*y[1]
vec3 cross (vec3 x, vec3 y) – векторное произведение x и y
genType normalize (genType x) – нормализация вектра
genType faceforward(genType N, genType I, genType Nref)
– если dot(Nref, I) < 0 возврашает N, в противном случае –N
genType reflect (genType I, genType N) – отражает луч от ”поверхности” с нормалью N.
Возвращает направление отражения: I – 2 * dot(N, I) * N
N должа быть заранее нормализована иначе будет неправильный результат.
genType refract(genType I, genType N, float eta) – преломление.
Преломляет вектор I с ”поверхностью” обозначенную нормалью N с преломлением eta.
Эквивалент
k = 1.0 - eta * eta * (1.0 - dot(N, I) * dot(N, I))
if (k < 0.0)
return genType(0.0)
else
return eta * I - (eta * dot(N, I) + sqrt(k)) * N

Функция ”тяжелая”.
8 ШЕЙДЕРЫ 70

8.2.22 Матричные функции


mat matrixCompMult (mat x, mat y)
– покомпонентное умножение компонентов матрицы.
Не является алгебраическим умножением матриц, для него используйте оператор *

8.2.23 Функции векторных отношений ( сравнений )


Операторы (<, <=, >, >=, ==, !=) зарезервированы для скалярных и булевых типов
данных, для векторов используются ниже перечисленные функции.
bvec lessThan(vec x, vec y) – возвращает покомпонентное сравнение x < y.
bvec lessThan(ivec x, ivec y)
bvec lessThanEqual(vec x, vec y) – возвращает покомпонентное сравнение x <= y.
bvec lessThanEqual(ivec x, ivec y)
bvec greaterThan(vec x, vec y) – возвращает покомпонентное сравнение x > y.
bvec greaterThan(ivec x, ivec y)
bvec greaterThanEqual(vec x, vec y) – возвращает покомпонентное сравнение x >= y.
bvec greaterThanEqual(ivec x, ivec y)
bvec equal(vec x, vec y) – возвращает покомпонентное сравнение x == y.
bvec equal(ivec x, ivec y)
bvec equal(bvec x, bvec y)
bvec notEqual(vec x, vec y) – возвращает покомпонентное сравнение x != y.
bvec notEqual(ivec x, ivec y)
bvec notEqual(bvec x, bvec y)
bool any(bvec x) – возвращает true если любой компонент true
bool all(bvec x) – возвращает true если все компоненты true
bvec not(bvec x) – возвращает true если любой компонент false

8.2.24 Функции выборки текстур


Функции выборки текстур доступны как в фрагментном так и вершинном шейдере.
Для всех функций ниже bias (смешение) является необязательным для фрагментных шейде-
ров и недоступен в вершинном шейдере.
Bias задает LOD (level of detail) текстур, в случае фрагментного шейдера это уровень
мипмап-карты.
Если Bias не указан то уровень берется автоматически ( что обычно предпочтительней).

Функции с суффиксом ”LOD” доступны только в вершинном шейдере.


Для LOD-функций параметр lod – это ручное задание уровня детализации.

Функции с суффиксом ”Proj” – проективные версии функций, в них текстурные координа-


ты (coord.s, coord.t) делятся на последний компонент coord, в случае vec4 третий компонент
игнорируется.
8 ШЕЙДЕРЫ 71

Параметр sampler2D sampler на самом деле целочисленный скаляр и указывает на кон-


кретный текстурный слот. Нумерация идет с 0.
vec4 texture2D (sampler2D sampler,vec2 coord [, float bias] )
vec4 texture2DProj (sampler2D sampler,vec3 coord [, float bias] )
vec4 texture2DProj (sampler2D sampler,vec4 coord [, float bias] )
vec4 texture2DLod (sampler2D sampler,vec2 coord, float lod)
vec4 texture2DProjLod (sampler2D sampler,vec3 coord, float lod)
vec4 texture2DProjLod (sampler2D sampler,vec4 coord, float lod)

и
vec4 textureCube (samplerCube sampler,vec3 coord [, float bias] )
vec4 textureCubeLod (samplerCube sampler,vec3 coord, float lod)

Вот и все встроенные функции GLSL.

Замечу, что, в отличие от ”настольной” версии, нет нескольких функций присутствующих


там, как, например, функции генерации шума noise, так что перенести шейдеры с помощью
паст-копи не всегда получается.
С другой стороны эти функции прекрасно реализуются средствами GLSL.
Да и некоторые производители добавляют свои функции, так что рекомендую переменные
называть с префиксом, иначе есть шанс попасть на ключевое слово.

8.2.25 Немного об оптимизации шейдеров


Старайтесь сократить размер шейдера, особенно фрагментного.
Часто многие расчеты можно вынести из фрагментного в вершинный.
Старайтесь не использовать ветвления в фрагментном шейдере.
Старайтесь использовать встроенные функции.

пример
float f1, f2, f3, f4;
float c = f1+f2+f3+f4; // медленно
//-------------
float в = dot(vec4(f1,f2,f3,f4),vec4(1.0)); // быстро

Старайтесь не использовать совсем маленькие полигоны ( микрополигоны ).


Сделайте несколько LOD-уровней меша, так как GPU просчитывает пикселы на стыке
полигонов несколько раз и смешивает, что при размере полигонов не больше нескольких
пикселей приводит к падению производительности.
Или ещё лучше – используйте объемное текстурирование (бамп, нормал-маппинг, парал-
лакс) для небольших объектов если нужна детализация, что будет работать намного быстрее,
чем использование микрополигонов.
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 72

8.2.26 Структура программы


Выполнение каждого шейдера начинается с функции ( точки входа ) main().
У функции main() нет возвращаемых значений.

Пример ( в данном случае фрагментный шейдер)


precision mediump float;
varying vec2 vTextureCoord;
uniform sampler2D sTexture;
void main() {
gl_FragColor = texture2D(sTexture, vTextureCoord);
}

9 Примеры шейдеров. Разбор работы


Далее начало фрагментного шейдера буду помечать как [fragment], вершиного [vertex].

Названия переменных:
Атрибуты буду называть с префикса "a"
Униформы c "u"
Варианты c "v"
и Семплеры с "s"или "t" (от Texture)

Отдельно буду описывать структуры данных для обработки,


они будут представлены в виде массивов с Java-синтаксисом.

Меш – набор одинаковых примитивов, например 3D модель или спрайт состоящий из


двух полигонов.
Количество примитивов от 1 до N.

Примитивы обрабатываемые шейдерами: треугольник (полигон), линия и точка.

Для полигона ( треугольника ):


Вершинный шейдер отрабатывает три раза – для каждой вершины.
Далее запускается фрагментный шейдер для каждого пикселя при растаризации данного по-
лигона.
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 73

Для линии:
Вершинный шейдер отрабатывает два раза – для каждой вершины.
Далее фрагментный для каждой точки линии. Линии могут быть разной толщины.

Для точки:
Точка задается одной вершиной, соответственно вершинный шейдер запускается один раз,
далее фрагментный шейдер один или больше раз.
Точки могут быть разного размера и включать в себя несколько пикселей.

Приступим.

9.1 Пример простейшего шейдера


Давайте рассмотрим работу простейшего шейдера:
Листинг 11: Простейший шейдер
[vertex]

highp attribute vec4 aPosition;


void main() {
gl_Position = aPosition;
}

[fragment]

void main() {
gl_FragColor = vec4(1.0);
}

Входные данные:

Массив атрибутов, в данном случае позиций вершин.


Данные индивидуальные для каждой вершины называются атрибутами.
float posattr[] = {
-0.75f, -0.75f, 0.0f, // первая вершина
0.0f, 0.75f, 0.0f, // вторая
0.75f, -0.75f, 0.0f // третья
};

Положение каждой вершины записаны в !данном! массиве тремя координатами, соответ-


ственно по одной для xyz.

Что это за массив?


9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 74

В таких массивах хранятся данные


о позициях вершин,
текстурных координатах
или нормалях.

Такие данные хранятся в массивах, где последовательно записываются данные по каждой


вершине.

Важно:

OpenGLES 2.0 может отрисовывать примитивы только заданные массивом или массивами
атрибутов вершин.
Другим способом задание примитивов невозможно.

Рисуется один полигон.

Что получим после отработки шейдера:

Получаем полигон закрашенный белым.


( конечно без стрелочек и точек )

Последовательность выполнения шейдера:

Весь код шейдера выполняется для каждой вершины отдельно.


Значение переменной aPosition будет индивидуальным для каждой вершины и будет браться
из массива posattr[].

Важно:

Как видите, данные положения вершины в массиве представляют собой трехкомпонент-


ный вектор, а aPosition четырехмерный.

Тут есть тонкость.

Для умножения координат вершины на матрицу 4x4 нужен четырехкомпонентный вектор


(x, y, z, 1.0), и специальная переменная gl_Position тоже использует четыре компонента.
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 75

Последняя хитрая координата называется w. Задает она проецирование в однородных


координатах, но в нашем случае она будет равна 1.
При дальнейшем преобразовании GPU разделит .xyz на .w, что при w=1 ничего не
изменит.
Для экономии памяти и уменьшения действий если задавать векторный атрибут меньшим
вектором, то последний его член автоматически задается равным единице.

Рассмотрим:

Первая вершина задается координатами (-0.75f, -0.75f, 0.0)

GPU считывает первые три члена массива posattrх[] и записывает в aPosition

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),

для второй (0.0, 0.75, 0.0, 1.0),


для третей (0.75f, -0.75f, 0.0, 1.0).

Напомню что gl_Position специальная переменная, которую условно можно считать "воз-
вращаемым значением" вершинного шейдера.

В ней возвращается позиция вершины в экранных координатах OpenGL.

После этого GPU проверит, находится ли наш полигон в видимой зоне экрана ( или
буфера ).
Если не видим, то на этом всё для полигона закончится.
Если видим, то занесет его данные в специальный буфер.
Как видите, определить видим полигон или не видим до выполнения кода вершинного
шейдера GPU не может – поэтому не рисуем заведомо невидимые объекты, так как для них
все равно будет отрабатывать вершинный шейдер.

Важно:

Существует главное ограничение на работу всех шейдеров:

Откуда считываем – писать нельзя, куда пишем – читать нельзя.


9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 76

То есть нельзя, например, рендерить в текстуру и читать из неё одновременно.


Приведёт от глюков до падения драйверов.

Второе ограничение – отсутствие какой либо связи между ядрами GPU.


Из чего следует, что мы не можем определить, что "насчитали" соседние блоки.

Связи между отдельными "потоками просчета" вершинного шейдера нет, так как совре-
менный GPU имеет несколько ядер ( а настольные – уже тысяч ядер ), которые параллельно
обрабатывают свой набор данных, и если бы связь была – пришлось бы делать синхрониза-
цию, что убивало бы все распараллеливание.

Далее идёт – GPU преобразовывает данные к экранным координатам и начинает расте-


ризацию.
То есть просчитывает отдельные пиксели.

Для каждого пикселя отдельно выполняется код фрагментного шейдера.

В нашем случае:

gl_FragColor = vec4(1.0);

что соответствует gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);

То есть каждому каналу мы присваиваем 1.0, что соответствует белому цвету.

Напомню, что специальная переменная gl_FragColor,


которую условно можно считать "возвращаемым значением" фрагментного шейдера,
в ней возвращается значение цвета пикселя в формате RGBA.

Вот и всё что делает данная шейдерная программа.

9.2 Varying
Давайте теперь разберем что такое тип varying и для чего они нужны.

Varying. Входные данные


//массив позиций вершин
float posattr[] = {
-0.75f, -0.75f, 0.0, // первая вершина
0.0, 0.75, 0.0, // вторая
0.75f, -0.75f, 0.0 // третья
};
// еще один массив c цветами вершин
float colorattr[] = {
1.0, 0.0, 0.0, // первая вершина
0.0, 1.0, 0.0, // вторая
0.0, 0.0, 1.0 // третья
};
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 77

Два массива можно записать как один


float colorposattr[] = {
-0.75f, -0.75f, 0.0, 1.0, 0.0, 0.0, // позиция + цвет для каждой вершины
0.0, 0.75, 0.0, 0.0, 1.0, 0.0,
0.75f, -0.75f, 0.0, 0.0, 0.0, 1.0
};

То есть значения атрибутов записываются сначала для первой вершины, потом атрибуты
второй и так далее.
Это и есть VAO массив.
Массивы со структурами атрибутов ( VAO ) обрабатываются быстрее, чем набор отдельных
массивов.
И быстрее, чем индексированные массивы.
( про массивы, точнее буферы атрибутов, в следующей части статьи )

Как и в прошлом примере рисуем один полигон.

Листинг 12: Varying. Шейдерная Программа


[vertex]

attribute highp vec4 aPosition;


attribute lowp vec4 aVertColor;

varying lowp vec4 vColor;

void main() {
vColor = aVertColor;
gl_Position = aPosition;
}

[fragment]

varying lowp vec4 vColor;

void main() {
gl_FragColor = vColor;
}

Запускаем и получаем такую картинку:


9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 78

Как видите мы получили линейную интерполяцию.

Как это работает:

Перед каждым выполнением фрагментного шейдера GPU вычисляет интерполяцию зна-


чений вершин для положения пикселя T по формуле:

𝐷 = 𝑑1 + 𝑑2. . . + 𝑑𝑁
𝑤1 = (1 − 𝑑1/𝐷)/(𝑁 − 1)
𝑤2 = (1 − 𝑑2/𝐷)/(𝑁 − 1)
...
𝑤𝑁 = (1 − 𝑑𝑁/𝐷)/(𝑁 − 1)
𝑣𝑇 = 𝑤1 * 𝑣1 + 𝑤2 * 𝑣2. . . + 𝑤𝑁 * 𝑣𝑁

Где:
𝑁 – количество вершин примитива
𝑑1, 𝑑2, 𝑑𝑁 – расстояние между вершиной и точкой T.
𝑣𝑇 – позиция вершины.

По этой же формуле будут линейно интерполироваться все данные, переданные через


varying-переменные из вершинного в фрагментный шейдер.

Такие как текстурные координаты ( нам же нужны координаты в конкретной точке поли-
гона, а не координаты вершины ), нормали полигонов, цвет вершин и т.д.

Проще говоря – в varying-переменную фрагментного шейдера попадает интерполирован-


ное значение этой переменной со всех вершин примитива для конкретного пикселя.

Все делается аппаратно на GPU и программист никак не может повлиять на это.

Минимальное количество поддерживаемых varying-переменных по стандарту – 8 четы-


рехмерных векторов.

Если мы хотим передать определенное значение без интерполяции – то его придется


задать для всех вершин примитива.
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 79

9.3 Uniforms
Теперь рассмотрим другой тип переменных – униформы (uniforms).

В отличие от атрибутов униформы едины для всех вершин и фрагментов меша.

Униформы могут использоваться как в вершинном так и фрагментном шейдерах.


Рассмотрим пример.
Uniforms. Входные данные
//массив координат вершин
float posattr[] = {
-0.75f, -0.75f, 0.0, // первая вершина
0.0, 0.75, 0.0, // вторая
0.75f, -0.75f, 0.0 // третья
};

//Uniform - переменные
float uColor[] = {1.0f, 1.0f, 0.0f} // цвет
float uTrans[] = {0.5f, 0.0f} // сдвиг

Листинг 13: Uniforms. Шейдерная программа


[vertex]

attribute highp vec4 aPosition;


uniform highp vec2 uTrans;

void main() {
gl_Position = aPosition;
gl_Position.xy += uTrans;
}

[fragment]

Uniform highp vec3 uColor;

void main() {
gl_FragColor = vec4(uColor,1.0);
}

Получим такую картинку ( желтый полигон ):


9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 80

Как это работает:

У каждой вершины меняем координаты, прибавляя uTrans.xy к aPosition.xy,


тем самым смещая весь меш ( в данном случае полигон ).
uTrans един для всех вершин меша.

Код
aPosition.xy += uTrans; // прибавляем к aPosition.xy uTrans.xy
...
// это можно расписать как:
aPosition.x = aPosition.x + uTrans.x;
aPosition.y = aPosition.y + uTrans.y;

Далее в вершинном шейдере задаем цвет gl_FragColor ( значение, которое запишется в


фреймбуфер ) равным uColor:

Код
gl_FragColor = vec4(uColor,1.0);
// преобразовываем vec3 к vec4 добавлением альфа-канала и задаем цвет.
// в данном случае R=1.0,G=1.0,B=0.0,A=1.0

// Можно расписать как:


gl_FragColor = vec4(uColor.r,uColor.g,uColor.b,1.0);

В общем случае через униформы передаем все значения одинаковые для всего меша,
такие как
матрицы преобразований,
данные о источниках освещения,
данные материала,
время,
трансформации,
в общем всё то, что не меняется от вершины к вершине.

Еще раз напомню:


Данные меняющиеся от вершине к вершине – атрибуты.
Данные одинаковые для всех вершин меша ( и фрагментов меша ) – униформы.
Передача данных из вершинного в фрагментный – через varying-переменные.
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 81

9.4 Трансформации примитивов в 2D


Посмотрим на рисунок:

Абсолютные координаты OpenGL ES от –1 до 1 по каждой оси !вне зависимости от про-


порций экрана!

Координата 0,0 приходится на центр.

9.4.1 Связь с координатами Android


Важно:

Координата Y с системе Android перевернута по отношению к абсолютным координатам


OpenGL, то есть идет не снизу вверх а сверху вниз.

Перевод координат Android в абсолютные OpenGL можно перевести следующим образом:


пускай aX,aY – точка в координатах Android.
screenW и screenH – ширина и высота сурфейса( GLsurface ) в пикселях.

Тогда абсолютные координаты OpenGL будут:


𝑋 = (2.0/𝑠𝑐𝑟𝑒𝑒𝑛𝑊 ) * 𝑎𝑋˘1.0
𝑌 = 1.0˘((2.0/𝑠𝑐𝑟𝑒𝑒𝑛𝐻) * 𝑎𝑌 )

Пускай размеры экрана 800x480

Нам нужно найти координаты 400,240 ( середина экрана )


Получаем:
X = (2.0/800) * 400 – 1.0 // = 0.0
Y = 1.0 - ((2.0/480) * 240) // = 0.0
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 82

То есть получили центр координат OpenGL, всё правильно.

Координаты считаются от размеров сурфейса OpenGL, а не экрана, соответственно коор-


динаты android тоже считаются относительно сурфейса.

Давайте добавим меш на котором будем рассматривать трансформации:

Он состоит из двух примитивов и четырех вершин ( у примитивов вершины v1 и v3


совпадают ).

Координаты вершин:
v1(-0.5, -0.5),
v2(-0.5, 0.5),
v3(0.5, 0.5) и
v4(0.5, -0.5)

Первый примитив v1,v3,v2.


Второй v1,v4,v3.

При обходе вершин против часовой стрелки по умолчанию считается, что полигон на-
правлен лицевой стороной на нас.

9.4.2 Трансформации
Нас интересуют три трансформации:
Translate – параллельный перенос
Scale – масштабирование ( скалирование )
Rotate – поворот
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 83

9.4.3 Translate
Пускай перенос задан двумерным вектором 𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒 (0.25, 0.25)

Для того, чтобы перенести вершины, нам нужно к каждой вершине покомпонентно при-
бавить вектор 𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒

𝑣 ′ = 𝑣 + 𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒;

Что можно расписать как


𝑣 ′ .𝑥 = 𝑣.𝑥 + 𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒.𝑥;
𝑣 ′ .𝑦 = 𝑣.𝑦 + 𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒.𝑦;

Где:
𝑣 ′ – новые координаты вершины,
𝑣 – вершина,
𝑣𝑇 𝑟𝑎𝑛𝑠𝑙𝑎𝑡𝑒 – вектор переноса.

Посмотрим что получилось:

Как видите, 𝑣1 перенеслась по координатам (-0.5+0.25, -0.5+0.25) = 𝑣 ′ (-0.25, -0.25)

9.4.4 Scale
Для того, чтобы проскалировать, нужно умножить вектор вершины на вектор скалирова-
ния.

Пусть скалирование ( масштабирование ) задано двумерным вектором 𝑣𝑆𝑐𝑎𝑙𝑒(0.5, 0.5)


Тогда 𝑣 ′ = 𝑣 * 𝑣𝑆𝑐𝑎𝑙𝑒;
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 84

Что получилось:

То есть 𝑣1′ = 𝑣1 * 𝑣𝑒𝑐2(0.5, 0.5) = 𝑣1′ (−0.25, −0.25)

9.4.5 Rotate
Для того чтобы повернуть вершину ( вектор ) нужно:
𝑥′ = 𝑥 * 𝑐𝑜𝑠(𝑡) − 𝑦 * 𝑠𝑖𝑛(𝑡)
𝑦 ′ = 𝑥 * 𝑠𝑖𝑛(𝑡) + 𝑦 * 𝑐𝑜𝑠(𝑡)

Где 𝑡 – угол поворота в радианах.


Поворот происходит относительно начала координат!
Поворот происходит против часовой стрелки.
Пускай хотим повернуть меш на 45 градусов, что соответствует 𝜋/4 в радианах.
𝑣.𝑥′ = 𝑣.𝑥 * 𝑐𝑜𝑠(𝑡) − 𝑣.𝑦 * 𝑠𝑖𝑛(𝑡)
𝑣.𝑦 ′ = 𝑣.𝑥 * 𝑠𝑖𝑛(𝑡) + 𝑣.𝑦 * 𝑐𝑜𝑠(𝑡)

Рисунок:
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 85

А что делать если мы хотим повернуть во круг точки отличной от начала координат?
Для этого нам сначала нужно сделать перенос а уже потом поворот.

Например мы хотим повернуть относительно точки 𝑣1 на 45 градусов.


Для этого нужно чтобы координаты вершины 𝑣1 были равны 0,0 – соответственно весь
меш нужно перенести на координаты обратные 𝑣1.
𝑣1′ = 𝑣1 − 𝑣1
𝑣2′ = 𝑣2 − 𝑣1
𝑣3′ = 𝑣3 − 𝑣1
𝑣4′ = 𝑣4 − 𝑣1
...
Далее сделать поворот.
Рисунок:

Далее нужно вернуть начальные координаты меша


𝑣1′ = 𝑣1 + 𝑣1′′
𝑣2′ = 𝑣2 + 𝑣1′′
𝑣3′ = 𝑣3 + 𝑣1′′
𝑣4′ = 𝑣4 + 𝑣1′′

Где 𝑣1′′ – первоначальные координаты вершины 𝑣1

Вот что получилось:


9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 86

Важно:

Как видите для трансформаций важна последовательность действий.

Для поворота, скалирования и переноса меша вокруг свое оси последовательность дей-
ствий такая:
1. поворачиваем
2. cкалируем
3. переносим

9.4.6 Пример шейдера для простой трансформации в 2D

Листинг 14: Простая трансформация 2D. Входные данные


//массив позиций вершин
float posattr[] = {
-0.75f, -0.75f, 0.0, // первая вершина
0.0, 0.75, 0.0, // вторая
0.75f, -0.75f, 0.0 // третья
};

//Uniform - переменные
float uRotate = Pi/2.0; // поворот, 90градусов
float uScale[] = {1.0, 1.0} // скалирование, двумерный вектор
float uTranslate[] = {-0.5, 0.0} // перенос, двумерный вектор

Листинг 15: Простая трансформация 2D. Код шейдера


[vertex]

attribute highp vec4 aPosition;


uniform highp vec2 uTranslate;
uniform highp vec2 uScale;
uniform mediump float uRotate;

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]

uniform highp vec3 uColor;

void main() {
gl_FragColor = vec4(uColor,1.0);
}
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 87

Результат:

Как это работает:


Объявляем двумерный вектор и поворачиваем вершину.
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


gl_Position = vec4(pos,aPosition.zw);

// Можно vec4(pos,aPosition.zw) расписать как


gl_Position = vec4(pos.x, pos.y, aPosition.z, aPosition.w);

Как видите довольно много действий и мало универсальности.


Например, этот шейдер не подходит если мы хотим изменить последовательность транс-
формаций или увеличить их число.
Что же делать?

Использовать матрицы трансформаций!


При использовании матриц трансформаций код:
uniform highp vec2 uTranslate;
uniform highp vec2 uScale;
uniform mediump float uRotate;
...
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);

Можно заменить на одну строчку:


9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 88

uniform mat4 uVertMatrix;


...
gl_Position = uVertMatrix * aPosition;

При этом не накладывается никаких ограничений на порядок и количество трансформа-


ций.
Если не знаете что такое матрицы...
то условно можно сказать, что в матрице ”аккумулируются”, ”накапливаются” трансфор-
мации, из сложных преобразовываясь в простые.
То есть нам никто не мешает сделать любую последовательность трансформаций.

Под Android есть специальный класс для работы с матрицами – android.opengl.Matrix.


Он же содержит функции для получения матрицы проекции ( камеры ).

Код в Android-программе
private final float[] VertMtx = new float[16]; // матрица преобразований

Matrix.setIdentityM(VertMtx, 0); // получаем единичную матрицу ( сбрасываем все изм


енения )
Matrix.translateM(VertMtx, 0, x, y, z); // переносим координаты
// первый параметр – ссылка на результирующею матрицу, второй – отступ от начала ма
ссива, и координаты x, y, z

Matrix.scaleM(VertMtx, 0, x, y, z);

Matrix.rotateM(VertMtx, 0, d, 0, 0, 1.0f); // третий параметр – угол поворота,


// последнии три параметра – задача оси вокруг которой будет поворот, в данном случ
ае z

//и передаем матрицу в наш шейдер:


GLES20.glUniformMatrix4fv(uVetMtxHandle, 1, false, VertMtx, 0);

Важно:
преобразовывать матрицу с помощью класса Matrix нужно в обратном порядке.
Для последовательности преобразования поворачиваем −→ cкалируем −→ переносим
нужно
перенести =⇒ скалировать =⇒ повернуть.

С преобразованиями матриц удобно работать через стек fifo.

Как видите с матрицами намного удобней, чем с отдельными преобразованиями и гиб-


кость решения намного выше.

9.5 Пример. Текстуры


Нарисуем текстурированный спрайт в 2D ( вернее с Z-координатой равной 0.0 )

Атрибуты вершин:
Координаты вершин, 2 float-а - a_vertex.
Текстурные координаты, 2 float-a - a_texcoord.
Записаны в массиве:
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 89

Листинг 16: Текстуры. Входные данные. Aттрибуты вершин


float quadv[] = {
-1, 1, 0, 0, // первая вершина
-1, -1, 0, 1, // вторая вершина
1, 1, 1, 0, // . . .
-1, -1, 0, 1, // . . .
1, -1, 1, 1, // . . .
1, 1, 1, 0 // шестая вершина
};

Униформы (Входные данные):


Целочисленный указатель на текстурный слот
sampler2D s_Texture1.
Матрица преобразования
u_VertMatrix.

Пускай рисуем спрайт, состоящий из двух полигонов.


Соответственно обрабатываем 6 вершин.

Листинг 17: Текстуры. Код шейдера


[vertex]
attribute highp vec4 a_vertex;
attribute mediump vec2 a_texcoord;
uniform mat4 u_VertMatrix;
varying mediump vec2 v_texcoord;

void main()
{
v_texcoord = a_texcoord;
gl_Position = u_VertMatrix * a_vertex;
}

[fragment]

uniform sampler2D t_texture1;


varying mediump vec2 v_texcoord;

void main()
{
gl_FragColor = texture2D(t_texture1, v_texcoord);
}

Получаем что-то вида:


9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 90

Как это работает:

a_vertex задает координаты вершины, с ним я думаю все понятно.


Специально привел название отличное от других примеров, чтобы не пугались, если
будете разбирать другие шейдеры.
aPosition, a_vertex, VertPos или еще как – название не важно.
Главное, чтобы мы знали, что там хранится.
В данном случае значения будут значения вида vec4(Xn, Yn, 0.0, 1.0), так как двумерный
атрибут преобразуется к четырехмерному.

Далее:

Как видите из массива quadv[] координаты вершин от –1 до 1 по каждой оси, что соот-
ветствует всему экрану.
Координаты текстур перевернуты по оси Y так как в OpenGLES на Android эта ось пере-
вернута.

u_VertMatrix – матрица, в данном случае хранит матрицу преобразований.

Вообще, использовать или не использовать матрицы решаем мы сами.


Никаких обязательных матриц нет.
Просто, часто с ними проще работать.

Я использовал матрицу 4x4, хотя, в данном случае, хватило бы и 3x3 ( для 2D ).

Умножение матриц 4x4 и 3x3 на столбец занимают один цикл.

А если использовать 3x3, то ещё потом придётся преобразовать vec3 к vec4 для записи в
gl_Position, на что как раз уйдет лишний цикл.

gl_Position = u_VertMatrix * a_vertex;

Умножаем матрицу на вершину, преобразованные координаты пишем в gl_Position.


Напомню, матрица*столбец != столбец*матрицу

Текстурный атрибут a_texcoord передаем в фрагментный шейдер через varying vec2 v_texcoord:
v_texcoord = a_texcoord;

Это нужно для того чтобы GPU интерполировал текстурные координаты между верши-
нами и мы на входе фрагментного шейдера получили координаты для конкретной точки.

Далее:

Фрагментный шейдер состоит из одной строчки:


gl_FragColor = texture2D(t_texture1, v_texcoord);

Значения текстуры по заданным текстурным координатам получаем с помощью функ-


ции texture2D(sampler s,vec2 с), первый параметр которой sampler2D, задающий текстурный
слот, и второй параметр двумерные координаты текселя на текстуре.
Возможен третий параметр, он задает уровень лода ( мипмапа ) для конкретной операции.
9 ПРИМЕРЫ ШЕЙДЕРОВ. РАЗБОР РАБОТЫ 91

Если его не указать то уровень детализации берется автоматически ( чаще лучше не


трогать =).
sampler2D на самом деле это целое число, нумерация слотов идет с 0 и точно до 31, то
есть, передавая из нашей программы в шейдер sampler2D равный двум, мы указываем на
GL_TEXTURE2 )

Операция чтения текселя по другому называется текстурной выборкой.


Многие GPU умеют делать несколько выборок одновременно для каждого шейдерного
процессора за такт.
v_texcoord приходит в фрагментный шейдер интерполированным между вершинами, то
есть в нем содержится значение координат для конкретной точки.
Далее значение пикселя записываем в gl_FragColor.

9.6 Пример. Несколько текстур. Смешивание


Входные данные как и в предыдущем примере, но добавим ещё несколько текстур. Далее
по тексту буду называть ссылки sampler2D не семплерами, а текстурами.

Пускай есть RGBA текстуры t_texture1, t_texture2.


Пускай есть одноканальная текстура t_mix1, данные на канале r.

Текстуры используют одинаковые координаты v_texcoord.

Листинг 18: Нескололько текстур. Смешивание. Код шейдера


[vertex]

highp attribute vec4 a_vertex;


mediump attribute vec2 a_texcoord;
uniform mat4 u_VertMatrix;
mediump varying vec2 v_texcoord;

void main()
{
v_texcoord = a_texcoord;
gl_Position = u_VertMatrix * a_vertex;
}

[fragment]

uniform sampler2D t_texture1;


uniform sampler2D t_texture2;
uniform sampler2D t_mix1;
mediump varying vec2 v_texcoord;

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);

Получаем значения текселей в двух текстурах.

Далее получаем скаляр tMix:


lowp float tMix = texture2D(t_mix1, v_texcoord).r;

.r что мы получаем только канал R или первый член вектора.


С помощью функции линейного смешивания mix(color1, color2, tMix) смешиваем значе-
ния color1 и сolor2 пропорционально значению tMix.

9.7 Пример. Анимация текстуры


Входные данные:

Атрибуты вершин:
Координаты вершины, 2 float-а - a_vertex.
Текстурные координаты, 2 float-a - a_texcoord.
Записаны в массиве:

Листинг 19: Анимация текстуры. Входные данные. Атрибуты вершин


private float quadv[] = {
-1, 1, 0, 0, // первая вершина
-1, -1, 0, 1, // вторая вершина
1, 1, 1, 0, // . . .
-1, -1, 0, 1, // . . .
1, -1, 1, 1, // . . .
1, 1, 1, 0 // шестая вершина
};

Униформы:
Целочисленный указатель на текстурный слот – sampler2D s_Texture1.
Матрица преобразования – u_VertMatrix.
10 ЗАГРУЗКА И КОМПИЛЯЦИЯ ШЕЙДЕРОВ 93

Двумерный вектор сдвига ( анимации ) текстуры – u_AnimVector.


Время – u_Time

Пускай рисуем спрайт, состоящий из двух полигонов.


Соответственно обрабатываем 6 вершин.

Листинг 20: Анимация текстуры. Код шейдера


[vertex]

highp attribute vec4 a_vertex;


mediump attribute vec2 a_texcoord;
highp uniform mat4 u_VertMatrix;
mediump varying vec2 v_texcoord;
highp uniform vec2 u_AnimVector;
highp uniform float u_Time;

void main()
{
v_texcoord = a_texcoord + (u_AnimVector*u_Time);
gl_Position = u_VertMatrix * a_vertex;
}

[fragment]

uniform sampler2D t_texture1;


mediump varying vec2 v_texcoord;

void main()
{
gl_FragColor = texture2D(t_texture1, v_texcoord);
}

В общем случае, наша задача просто смещать текстурные координаты.


Я специально не вынес u_AnimVector*u_Time из шейдера в программу, так как вместо
u_AnimVector можно использовать вершиный атрибут для того, чтобы текстура перемеща-
лась неравномерно по разным вершинам, таким способом часто делают воду в ручьях или
речках ( например в Skyrim ). Или поток лавы будет выглядеть красиво при неравномерности
смещения текстуры.

10 Загрузка и компиляция шейдеров


Шейдеры в контексте загрузки могут быть в двух видах:
1) в виде исходного текста GLSL.
2) в скомпилированном виде.

В виде исходного текста для программиста шейдер представляет из себя строку ( или
массив символов в случае Си ).

Для компиляции шейдера, первым делом, нам нужно создать шейдер и получить ссылку
на него:
int shaderID = GLES20.glCreateShader(int shaderType);
10 ЗАГРУЗКА И КОМПИЛЯЦИЯ ШЕЙДЕРОВ 94

Где shaderType может быть двух видов:


GLES20.GL_VERTEX_SHADER – для вершинного
GLES20.GL_FRAGMENT_SHADER – для фрагментного

Далее нужно привезать исходник к shaderID и скомпилировать его:


GLES20.glShaderSource(int shaderID,String Source);
GLES20.glCompileShader(shaderID);

Важно:
Вызов GLES20.glCompileShader приводит к загрузке компилятора шейдера ( который сжи-
рает достаточно много памяти и долгий ).

Проверить успешность компиляции шейдера можно таким способом:


int[] compiled = new int[1];
GLES20.glGetShaderiv(shaderID, GLES20.GL_COMPILE_STATUS, compiled, 0);
if (compiled[0] == 0) {
Log.e("Error", GLES20.glGetShaderInfoLog(shaderID));
GLES20.glDeleteShader(shader);
}

Где GLES20.glGetShaderInfoLog вернет лог ошибок на этапе компиляции.


Информативность лога зависит от драйверов, но обычно достаточная для получения типа
ошибки и строчки её возникновения.

В случае ошибки, если компиляция шейдера не удалась, его рекомендуется уничтожить


вызовом
GLES20.glDeleteShader();

Если все предыдущие этапы завершились нормально – можно приступать к линковке


шейдерной программы ( далее просто программы ).

Создаем программу и получаем ссылку на нее:


prog_id = GLES20.glCreateProgram();

привязываем шейдеры к программе:


GLES20.glAttachShader(prog_id, vertexShader);
GLES20.glAttachShader(prog_id, pixelShader);

и линкуем программу:
GLES20.glLinkProgram(prog_id);

Всё, шейдерная программа скомпилирована и готова к работе.

Важно:
Осталось подчистить за собой хвосты. Так как в памяти у нас еще висит компилятор его
нужно выгрузить:
GLES20.glReleaseShaderCompiler();

Я думаю понятно, что выгружать компилятор нужно после того, как мы скомпилируем
все нужные нам шайдеры, а не после каждого.

Важно:
Один и тот же шейдер может использоваться в нескольких программах.
Для этого его нужно просто прикрепить к нескольким программам и слинковать их.
11 ЗАГРУЗКА СКОМПИЛИРОВАННЫХ ШЕЙДЕРОВ 95

11 Загрузка скомпилированных шейдеров


Cкомпилированные шейдеры можно загрузить с помощью функций:
glShaderBinary (int n, int[] shaders, int offset, int binaryformat, Buffer binary,
int length);
glShaderBinary (int n, IntBuffer shaders, int binaryformat, Buffer binary, int
length);

Где:
Buffer binary – буфер с бинарными данными binaryformat – аппаратнозависимый формат,
обычно разный даже для разных моделей GPU одного производителя.
Ищется в документации от производителей.
n – количество шейдеров для загрузки
shaders – массив указателей шейдеров
offset – смещение относительно Buffer binary
binaryformat – вендерозависимый формат ( смотреть у производителя GPU )
Buffer binary – буффер с данными шейдера.
length – размер буфера.

Как видите glShaderBinary позволяет загружать несколько шейдеров одновременно.

Плюсы загрузки в бинарном формате:


Не требуется компиляция.
Загрузка сразу нескольких шейдеров.
Возможность ручной оптимизации шейдеров с помощью стороннего ПО.

Минусы:
Шейдеры в бинарном формате вендерозависимы и не совместимы между собой, даже
иногда среди разных чипов одного производителя.
Так что придется делать кучу сборок под все возможные конфигурации оборудования.

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

12 Выгрузка шейдеров
Для удаления шейдера нужно вызвать
glDeleteShader(int shaderID)

и отвязать шейдер от программ с помощью:


glDetachShader(int progID,int shaderID)

или удалить программу:


glDeleteProgram(int progID)

Важно:
glDeleteShader помечает шейдер на удаление, но шейдер не будет удален, пока он при-
креплен хотя бы к одной программе.
Для удаление шейдера его нужно отвязать от программ или удалить сами программы.
13 УСТАНОВКА ТЕКУЩЕГО ШЕЙДЕРА 96

13 Установка текущего шейдера


Установить текущий шейдер можно вызовом:
GLES20.glUseProgram(int progID);

В приложении возможен только один текущий шейдер одновременно.


То есть вся обработка примитивов будет происходить с текущей программой.

Важно:
Вызов glUseProgram "тяжелый", старайтесь делать как можно меньше переключений про-
грамм.

Обычно это болезнь старых движков, переключение материалов по каждому чиху, к ко-
торым примотали поддержку OpenGL 3.0+ или OpenGL ES 2.0+ без переписывания рендера
и менеджера рендеринга, и соответственно хорошо, если половина от реально возможной
производительности на выходе.

Когда будете писать свой рендер сортируйте порядок отрисовки в рамках шейдерной
программы и текстур, а не мешей.
То есть привязали шейдер, установили текстуры, закрепили атрибуты ( буферы ) – и
отрисовываем все меши для данного материала – и только потом переключение на новый.

14 Проверка правильности ( валидация ) шейдерной про-


граммы
На этапе компиляции шейдеров нельзя выявить все ошибки, часть из них возникает
только после установки атрибутов и униформ.
Например мы установили у двух самплеров ( текстурных указателей ) один и тот же
слот, что не отслеживается на этапе компиляции.
Или не тех форматов передаем значения в униформы.
Для проверки шейдера на корректность можно воспользоваться функцией:
void ValidateProgram( uint programID );

Получить возвращаемое значение можно с помощью GetProgramiv() с параметром VALIDATE_STATUS,


где значение True будет в случае успешной валидации.

Производить проверку имеет смысл только после расстановки униформ и атрибутов.

15 Общие рекомендации по загрузке шейдеров


!Старайтесь проверять шейдеры на возможность компиляции под разные платформы ( и
разные версии компиляторов внутри платформ )
!хотя бы перед релизом приложения.

!Все разработчики GPU предоставляют свои SDK, в состав которых входят компиляторы
и профайлеры для шейдеров.
16 СВЯЗЬ ДАННЫХ ПРИЛОЖЕНИЯ И ШЕЙДЕРА 97

!Очень часто бывает, что шейдер отказывается компилироваться на какой-либо платфор-


ме из-за более строгих правил синтаксиса.

!А на маркете счастливые владельцы этих устройств с удовольствием вам проставят кучу


единиц и "лестные" комментарии.

16 Связь данных приложения и шейдера


16.1 Получение указателей на атрибуты и униформы
Получение ссылок на атрибуты и униформы можно получить с помощью вызовов:
int AttribHandle = GLES20.glGetAttribLocation(progID, "имя␣атрибута"); // для атриб
утов
// и
int UniformHandle = GLES20.glGetUniformLocation(progID, "имя␣униформы"); // для уни
форм

Важно:
Имя не может быть структурой или частью вектора или матрицы.
Структуры нужно задавать по членам, например Light.Force, просто Light использовать
нельзя. И нельзя задавать имена типа vector.x или vector[0].

Соответственно, нужно получить все указатели на все атрибуты и униформы шейдерной


программы.
Ссылки уникальны только в рамках программы и вида (униформа или атрибут ), у двух
программ вполне могут быть одни и те же ссылки как и у униформы и атрибута одной про-
граммы.

Вызовы "тяжелые" ( так как происходит поиск по имени и настройка таблицы атрибу-
тов ) использовать на этапе загрузки, не использовать в цикле отрисовки, а то я реально
встречал в коде у вполне известного приложения эти вызовы в цикле отрисовки при каждом
переключении шейдера, что давало огромные тормоза.

16.2 Передача данных в шейдерную программу


Передачу атрибутов давайте лучше рассмотрим чуть ниже с буферами вершинных атри-
бутов, а сейчас передача униформ.

16.2.1 Передача униформ из приложения в шейдер


Важно:
Минимально стандартом GLES20 для униформ определена поддержка 128 четырехмерных
векторов для одной шейдерной программы ( то есть вершинные униформы + фрагментные ).

Вспомним каких типов у нас бывают униформы:


Униформы могут быть любого типа GLSL.
Включая скаляры, вектора, матрицы и структуры как целые так и с плавающей запятой.

Но в памяти устройства униформы представляют из себя специальные регистры опре-


деленного размера для быстрого доступа. Для нынешних мобильных GPU размер такого
16 СВЯЗЬ ДАННЫХ ПРИЛОЖЕНИЯ И ШЕЙДЕРА 98

регистра скорее всего 16 байт3 или 128 бит, что достаточно для хранения 4 float значений.

Одна униформа может занимать как один так и несколько таких регистров.
Например, mat4 займет четыре регистра, а mat3 три регистра ( три ячейки в регистрах
останутся пустыми, если драйвер не дополнит регистры скалярами )
То есть 128 четырехмерных векторов не обязательно значит 512 отдельных скалярных
униформ.

Если мы делаем отдельную скалярную униформу то она все равно займет один регистр
целиком.

Старайтесь упаковывать униформы вручную, если есть подозрения, что регистров может
не хватить, так как был случай, что драйвер не правильно упаковывал значения, ( вернее
разные драйвера по разному ), что приводило к нехватке регистров и соответственно прило-
жение не работало.

Пример упаковки varying значений ( для униформ тоже самое ):


varying vec4 a;
varying mat3 b;
varying vec2 c[3];
varying vec2 d[2];
varying vec2 e;
varying float f[3];
varying float g[2];
varying float h;

Расположение в памяти:
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;
. . .

Для передачи vec3 можно использовать процедуру:


glUniform3f(HandleEyePos , 0.0f, 2.0f, -1.0f);
16 СВЯЗЬ ДАННЫХ ПРИЛОЖЕНИЯ И ШЕЙДЕРА 100

Следующая группа функций служит для передачи массивов:


glUniform{1|2|3|4}{f|i}v ( int location, int count, Buffer params);
glUniform{1|2|3|4}{f|i}v ( int location, int count, float[] params, int offset);

Думаю каждую функцию не имеет смысл расписывать отдельно, главное отличие от преды-
дущих – передача массивов.

count – количество элементов в массиве для передачи, нумерация с 1.


params – буфер или массив с данными.
offset – отступ от начала массива.

Пример:
glUniform3fv(int location, int count, float[] v, int offset);

float[] Light = new float[9];


glUniform3fv(uLightHandle, 3, Light, 0); // передаем массив из трех элементов vec3

И последняя группа функций:


void UniformMatrixf234[f](int location, int count, boolean transpose, . . .)

Которая служит для передачи матриц и аналогична функциям передачи массивов, кроме
параметра transpose.
Если параметр transpose установлен в True, то будет передаваться транспонированная
матрица.

Важно:
Запись данных в таблицу униформ происходит в момент вызова glUniform*

Важно:
Функции передачи униформ обязательно должны соответствовать типу данных в шейде-
ре.
То есть нельзя передавать в vec3 с помощью void glUniform3i(), так как это разные типы
данных и результат будет непредсказуемым.
Или vec4 передавать glUniform13fv().

Важно:
sampler-переменные ( указатель на текстурный слот ) в шейдер можно передавать только
через Uniform1i и Uniform1iv, иначе будет ошибка.

16.2.2 Общие рекомендации по униформам


Не старайтесь придумать униформы на все случаи жизни.
Передавайте только то, что нужно передавать в данный момент для данного шейдера.
Универсальный шейдер – путь к тормозам и слайдшоу.
То есть, если у вас используется только один источник освещения, передавать восемь не
имеет смысла.
Не выносите все числовые значения в униформы, если какой то параметр можно сделать
константой в шейдере – делайте константой.
На все случаи все равно не запасетесь, а расчеты это замедлит ( и займет лишнее место
в таблице униформ).
В самом шейдере ограничивайте класс точности униформ по самому минимуму, так как
может уменьшить количество расчетов.
17 БУФЕРЫ АТРИБУТОВ ВЕРШИН. VBO 101

Давайте нормальные имена, желательно с префиксом.


Например, конструкция float sin = sin(time) на PowerVR и Adreno не вызовет ошибки, а
на Mali у вас не скомпилируется шейдер.
Производители имеют особенность делать дополнительные функции за рамками стан-
дартных и кто знает не совпадут ли у вас имена с ними.

17 Буферы атрибутов вершин. VBO


Вершинные атрибуты и тем самым примитивы в OpenGL ES 2.0 можно задать только
одним способом – через массивы вершинных атрибутов. То есть в GLES20 нет процедур из
старых версий стандарта для задачи вершин и их параметров.

Массив вершинных атрибутов можно рассматривать как массив структур, каждая из


которых задает параметры своей вершины.
Например, нам нужно задать для каждой вершины позицию, цвет и текстурные коорди-
наты.

Тогда структура будет выглядеть так:


Vertex {
Position,
Color,
TexCoord
}

Порядок записи вершинных атрибутов в структуре никак не регламентируется и может


быть любым.
Как и нет каких либо обязательных атрибутов.
То есть теоретически количество атрибутов может быть нулевым – только мы просто
ничего не нарисуем с такими “данными”.

Для задачи одного полигона нам потребуется три такие структуры, такой массив будет
выглядеть так:
{Vertex1, Vertex2, Vertex 3}.

Минимально стандартом определено восемь highp vec4 атрибутов.

То есть вершинные атрибуты всегда задаются для каждой вершины индивидуально.


Например у нас 100 спрайтов.
Пускай текстурные координаты у всех спрайтов одинаковы.
Но нам все равно придется задать текстурные координаты для каждой вершины, то есть
мы продублируем данные 100 раз!

Сократить лишние расходы можно и нужно.


Получается, что у нас реально только четыре текстурные координаты, так что мы можем
вынести их в константы шейдера – и передавать в атрибуте только номер координаты.
Для этого в данном случае нам хватит одного байта ( а не двух float ).

Для программиста и реализации OpenGL массив вершинных атрибутов всегда представ-


ляет из себя массив байтов.
Массив может быть смешанного типа, то есть а массиве возможны любые комбинации
типов разрешенные для данных вершинных атрибутов.
17 БУФЕРЫ АТРИБУТОВ ВЕРШИН. VBO 102

Как помните вершинные атрибуты в шейдере могут быть только float, vec2, vec3, vec4,
mat2, mat3 и mat4.

Важно:
В шейдере OpenGL ES 2.0 атрибуты не могут быть целочисленными данными!

Если мы хотим передать целое в атрибуте, то в шейдере придется преобразовывать float


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

Задавать атрибуты в массиве можно типами BYTE, UNSIGNED_BYTE, SHORT, UNSIGNED_SHORT,


FIXED, и FLOAT.

То есть в массиве возможна данная последовательность:


. . .
FLOAT,FLOAT,FLOAT,UNSIGNED_BYTE,UNSIGNED_BYTE,FLOAT,BYTE, // первая вершина
FLOAT,FLOAT,FLOAT,UNSIGNED_BYTE,UNSIGNED_BYTE,FLOAT,BYTE, // вторая вершина
. . .

Важно:
Данные для каждой вершины ( структуры ) массива должны быть одного типа и одина-
ково расположены.

Некоторые GPU поддерживают для атрибутов HALF_FLOAT, но так как это уже расши-
рение рассматривать его не буду.

Важно:
Если в типом float всё понятно, передали из приложения float и получили в шейдере float
то, что происходит при передаче например byte? Ведь в шейдере атрибуты могут быть только
типом с плавающей запятой?

Например, мы передаем значение тип UNSIGNED_BYTE со значением 255 в атрибут


mediump float B;

Что мы получим в B?

Всё зависит от способа передачи атрибута.

Можем получить как 1.0 так и 255.0

Есть два способа передачи атрибутов:


1) по значению
2) в нормализованном виде.

При передаче "по значению" в шейдере мы получим соответствующее float.


Например передав целое 56 мы получим float 56.0

Если мы передадим в нормализованном виде UNSIGNED_BYTE со значением 56, то в


шейдере получим 0.21875,
то есть при передаче в нормализованном виде значение скалируется от 0.0 до 1.0 для
беззнаковых типов и от -1.0 до 1.0 для знаковых.
18 ТАБЛИЦА ССЫЛОК АТРИБУТОВ И ТАБЛИЦА АТРИБУТОВ 103

18 Таблица ссылок атрибутов и таблица атрибутов


Для правильной работы шейдера нам нужно настроить таблицу ссылок атрибутов.

В таблицу данных атрибутов значения попадают при отрисовке примитивов ( не раньше


=) ), физически представляет из себя набор специальных регистров GPU.

В таблице ссылок хранятся адреса начала массивов в памяти устройства,


смещение, тип и размер данных для выборки и
смещения в массиве для одной вершины.

Как это работает:


Первый регистр.
адрес начала массива – 1000
тип и размер данных – float[2], итого 8байт, смещение 0
размер смещения для каждой вершины в данном массиве – 16байта

Для первой вершины GPU считает байты от 1000 до 1007, преобразует их во внутренний
формат и запишет в первый регистр для последующей обработки шейдером.
Для второй вершины сделает сдвиг начала на размер данных вершины 1000+15, и считает
данные с 10015 по 10022 и преобразует их во внутренний формат и запишет в первый регистр
и так далее.
19 МАССИВЫ В ПАМЯТИ ПРИЛОЖЕНИЯ. VBO 104

Как видите из рисунка, предположим, что в данном массиве пять атрибутов – [vec2, vec2,
float, float, float], так как, например, можно задействовать отдельно первые четыре байта и
получить float атрибут, то есть отношение байтов к какому либо типу условно.
То есть два элемента short при загрузке преобразуются в vec2, скаляры ubyte, byte и
short преобразуются к float, и все значения атрибутов в зависимости от типа передачи могут
быть нормализованы.

Так как массив представляет собой набор данных, мы сами должны отслеживать размер-
ность и порядок данных и задавать правильный тип и смещение для элементов.

Не обязательно задействовать все данные в массиве, можно подключить любой элемент


отдельно от

Важно:
Для атрибутов автоматической упаковки в отличие от униформ и варингов не происходит!
Только вручную.
Так что старайтесь максимально упаковывать данные.

В примере на рисунке очень плохое расположение атрибутов =).


Эти атрибуты займут 5 регистров ( из гарантированных восьми ), при этом регистры
останутся наполовину пустыми.

Второй и третий атрибуты (short[2] и short ) можно объединить в один, получив на


выходе vec3, 4 и 5 атрибуты можно объединить в один vec2.
Уже получим не пять, а три атрибута.

Важно:
Данные для одного атрибута должны быть одного типа, например, упаковывать два byte
и short в один атрибут нельзя, на выходе получим мусор.
И, естественно, нельзя упаковывать данные из разных массивов.

Данные каждого регистра могут заполняться как из одного, так и из разных массивов (
любых типов ), но нельзя настраивать выборку одних и тех же данных.

19 Массивы в памяти приложения. VBO


Массивы атрибутов в памяти приложения ( в Java ) представляют из себя обычные
ByteBuffer.

Для работы сначала нам нужно загрузить данные в ByteBuffer или преобразовать любой
обычный массив.
В силу специфики Java не удобно работать со смешанными массивами ( с разным типом
элементов ) поэтому, рекомендую по возможности загружать такие массивы из файла уже
предпросчитанными ( то есть не использовать какие либо форматы - а сразу грузить массив
атрибутов ).

Пускай мы хотим сделать массив, который задает прямоугольный спрайт на весь экран.

Атрибуты которые нам нужны – позиции вершин в 2D и текстурные координаты.


19 МАССИВЫ В ПАМЯТИ ПРИЛОЖЕНИЯ. VBO 105

Пускай вершина задана двумя координатами float, текстурные координаты аналогично.

Объявим обычный float массив:


private float quadv[] =
{
-1, 1, 0, 0,
-1, -1, 0, 1,
1, 1, 1, 0,
-1, -1, 0, 1,
1, -1, 1, 1,
1, 1, 1, 0
};

Далее нужно получить Buffer на из массива ( в данном случае FloatBuffer, но ничто не


мешает использовать другие виды буферов ):
FloatBuffer mTriangleVertices;
mTriangleVertices = ByteBuffer.allocateDirect(quadv.length * 4).order(ByteOrder.
nativeOrder()).asFloatBuffer();
mTriangleVertices.put(quadv).position(0);

Где:
ByteOrder.nativeOrder() – задает порядок байтов, в данном случае системный.

В данном случае режимы бывают:


ByteOrder.BIG_ENDIAN - от старшего к младшему
и
ByteOrder.LITTLE_ENDIAN – от младшего к старшему.
ByteOrder.nativeOrder() – выбирает текущий системный способ расположения байтов.

Порядок байтов имеет смысл для многобайтовых типов переменных и задает порядок
записи в многобайтовых типах.
Почитать подробнее

ByteBuffer.allocateDirect(quadv.length * 4) – выделяет непрерывный блок памяти под наш


массив в байтах.

Это нужно, чтобы массив располагался одним блоком в памяти, а элементы float[] массива
могут быть разбросаны ( java не регламентирует расположение элементов, ни порядок слов
в переменных ) и самое главное: ByteBuffer - это единственный способ сделать простой
смешанный ( с разными типами данных ) массив.
Единственный параметр задает размер в байтах, а так как float занимает 4 байта нужно
длину массива quadv.length * 4.

mTriangleVertices.put(quadv).position(0) – записываем наш float[] массив в выделенный


блок и устанавливаем нулевую позицию.

Если расписать эти дествия по строчкам то получится:


FloatBuffer mTriangleVertices; // объявляем буффер
ByteBuffer bb = ByteBuffer.allocateDirect(quadv.length * 4); // выделяет "блок" пам
яти
bb.order(ByteOrder.nativeOrder()); // порядок байтов
mTriangleVertices = bb.asFloatBuffer();
mTriangleVertices.put(quadv); // записываем массив в "блок" памяти
mTriangleVertices.position(0);
20 БУФЕР ВЕРШИННЫХ АТРИБУТОВ В ПАМЯТИ GPU ИЛИ VBO 106

Важно:
Обязательно нужно устанавливать позицию массива на 0 ( buffer.position(0) ), так как
позиция при передаче массива считается его началом ( сишной функции передается указа-
тель на начало ).

Всё, массив в готов для работы.


Мы уже можем отрисовывать примитивы.

Но есть более быстроработающий способ – размещение массива в памяти ( или адресное


пространство ) GPU.
Оптимизация достигается удобным для конкретного GPU способом хранения данных и
кэшированием расчетов и иногда достигает нескольких раз, а в случае выделенной памяти
GPU может отличатся на порядок ( и я думаю такие чипы появятся в следующем поколении,
так как уже все упирается в память ).

Но массивы в памяти приложения тоже востребованы, с ними удобней работатьб когда в


них часто изменяем данные.

Теперь давайте рассмотрим, как передать массив в память GPU.

20 Буфер вершинных атрибутов в памяти GPU или VBO


Для начала работы с буфером в памяти GPU нужно его создать:
GLES20.glGenBuffers(int count,int] buffer_id, int offset);

где:
первый параметр – число генерируемых буферов.
второй – массив ссылок буферов
третий – отступ от начала массива.

Далее нужно выбрать активный массив:


GLES20.glBindBuffer(int target, int Buffer_id );

где:
Buffer_id – ссылка на буфер
target – тип буфера.
для GLES20 буферы бывают двух типов:
GL_ARRAY_BUFFER и
GL_ELEMENT_ARRAY_BUFFER.

Тип GL_ARRAY_BUFFER является обычным буфером вершинных атрибутов,


а тип GL_ELEMENT_ARRAY_BUFFER служит для описания буфера индексов вершин.
Давайте индексированные буферы рассмотрим в следующем разделе.

Теперь у нас выбран текущий буфер и можно передавать данные:


GLES20.glBufferData(int target, int size, Buffer b, int drawHint);

где:
target – тип буфера, GL_ARRAY_BUFFER или GL_ELEMENT_ARRAY_BUFFER,
size – размер в байтах,
20 БУФЕР ВЕРШИННЫХ АТРИБУТОВ В ПАМЯТИ GPU ИЛИ VBO 107

b – буфер данных,
drawHint – способ использования буфера.

drawHint бывает:
STATIC_DRAW – данные определяются раз и используются много раз без изменений.
DYNAMIC_DRAW –данные используются много раз и иногда переопределяются прило-
жением.
STREAM_DRAW – данные используются только несколько раз и часто переопределяются
приложением.

Эти определения влияют на скорость обработки данных, по скорости обычно быстрее


STATIC_DRAW, потом DYNAMIC_DRAW и уже STREAM_DRAW. Но, так как это дано на
откуп GPU и драйверам, не гарантируется, что STATIC_DRAW быстрее STREAM_DRAW
или что STREAM_DRAW медленнее STATIC_DRAW.

drawHint – "рекомендательный" параметр, никто не мешает менять STATIC_DRAW мас-


сив, но мы можем существенно потерять в скорости при неправильном определении цели
массива.

Данные передаются в момент вызова glBufferData.

Важно:
Если массив у вас используется только раз – вообще не имеет смысла переносить буфер
в память GPU, только затормозите приложение.
В этом случае используйте массивы в памяти приложения.

При вызове glBufferData для уже существующего массива уничтожает все старые данные
и пересоздает буфер.

Проверить на ошибки можно через glError.


При неудаче glBufferData возвратит OUT_OF_MEMORY.

Уничтожить буфер можно вызовом:


DeleteBuffers( int count, int] buffers_id, int offset );

где:
count – количество удаляемых буферов
buffers_id – массив ссылок
offset – смещение от начала массива

Теперь все строчки вместе:


ByteBuffer b;
. . .
int[] buffer_id = new int[1];
GLES20.glGenBuffers(1,buffer_id, 0);
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffer_id[0]);
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER,b.capacity(),b,GLES20.GL_STATIC_DRAW);
21 ИЗМЕНЕНИЕ ЧАСТИ ДАННЫХ БУФЕРА В ПАМЯТИ GPU 108

21 Изменение части данных буфера в памяти GPU


У любых типов буферов можно изменить часть данных.

Это делается с помощью вызова:


GLES20.glBufferSubData(int target, int offset, int size, Buffer b);

где:
target – тип буфера, GL_ARRAY_BUFFER или GL_ELEMENT_ARRAY_BUFFER
offset – смещении от начала буфера
size – количество заменяемых байтов
b – буфер с новыми данными

Замечу, что этот вызов "тяжелый", и если данные меняются очень уж часто – лучше
использовать данные в памяти приложения, а не VBO на стороне GPU.

22 Буферы индексов вершин


Давайте еще раз рассмотрим массив атрибутов из прошлого примера:
private float quadv[] = {
-1, 1, 0, 0, // вершина 1
-1, -1, 0, 1, // вершина 2
1, 1, 1, 0, // вершина 3
-1, -1, 0, 1, // вершина 4
1, -1, 1, 1, // вершина 5
1, 1, 1, 0 // вершина 6
};

Он задает спрайт состоящий из двух полигонов, для описания которых нам понадобилось
шесть вершин.

Но спрайт прямоугольный, и для его задачи нам, по идее, достаточно четырех вершин,
так как данные у третей – шестой и второй – четвертой вершин совпадают.

Давайте уберем совпадающие вершины 3 и 4:


private float quadv[] = {
-1, 1, 0, 0, // вершина 1
-1, -1, 0, 1, // вершина 2
1, -1, 1, 1, // вершина 3
1, 1, 1, 0 // вершина 4
};

Теперь, чтобы задать те же полигоны, нам понадобится следующая последовательность:


1-2-4 и 2-3-4, то есть можно сократить количество данных.

Массив данных описывающий такую последовательность и называется массивом индек-


сов вершин, а его значения – индексами.

Это, как видите, позволяет сильно оптимизировать, как объем данных, так и количество
расчетов. Так как для второго полигона вершины 2 и 4 уже рассчитаны – соответственно
можно рассчитать только вершину 3, а для остальных использовать кэш ( если железо ко-
нечно позволяет ).
23 ПРИВЯЗКА АТРИБУТОВ МАССИВОВ. ПОДКЛЮЧЕНИЕ И ОТКЛЮЧЕНИЕ АТРИБУТОВ109

Но это применимо только тогда, когда совпадает достаточное количество вершин у меша.
То есть, если задавать индексы для отдельностоящих полигонов ( например системы ча-
стиц ), у которых не совпадают вершины, мы наоборот замедлим приложение.

Некоторые GPU поддерживают тип Int, но так как это за рамками стандарта...

Массив индексов для данного спрайта будет выглядеть так:


private int qindex[] = {
0, 1, 3 // первый полигон
1, 2, 3 // второй полигон
};
//Индексация идет с нулевого элемента.

Ещё существует существенное ограничение на количество индексов в одном массиве,


так как по стандарту массив состоит из ushort элементов и, соответственно, максимальный
размер 65536 индексов вершин.
Замечу – не размер меша ( буфера индексов вершин ), а размер буфера атрибутов ( не
индексов ).

Почему тип int а не short?


Всё дело в том, что short в java знаковый тип и, соответственно, может принимать зна-
чения от -32,768 до 32,767, а не от 0 до 65к.

Пример инициализации массива индексов:


ByteBuffer b;
. . .
int] buffer_id = new int[1];
GLES20.glGenBuffers(1,buffer_id, 0);
GLES20.glBindBuffer(GLES20.GL_ELEMENT_ARRAY_BUFFER, buffer_id[0]);
GLES20.glBufferData(GLES20.GL_ELEMENT_ARRAY_BUFFER,b.capacity(),b,GLES20.
GL_STATIC_DRAW);

Всё отличие при инициализации в параметре GL_ELEMENT_ARRAY_BUFFER.

23 Привязка атрибутов массивов. Подключение и отклю-


чение атрибутов
После того, как мы загрузили массивы атрибутов ( будь те в памяти GPU или приложе-
ния ), нам требуется привязать атрибуты шейдера к данным в массиве.

Сначала нужно получить ссылку на атрибут в вершинном шейдере.


Для этого используется одна из двух функций:
aAtributeHandle = GLES20.glGetAttribLocation(int prog_id, String name);

где:
prog_id – ссылка на шейдерную программу,
name – имя атрибута в шейдере,
aAtributeHandle – ссылка на атрибут шейдера.

Функция возвращает -1 в случае ошибки


23 ПРИВЯЗКА АТРИБУТОВ МАССИВОВ. ПОДКЛЮЧЕНИЕ И ОТКЛЮЧЕНИЕ АТРИБУТОВ110

и
glBindAttribLocation(int prog\_id, int index, String name);

где:
index – номер за каким мы хотим закрепить атрибут.

вся разница в моменте вызова функций:


glBindAttribLocation вызываеться ДО линковки программы (glLinkProgram), а glGetAttribLocation
ПОСЛЕ линковки.

Если вызвать glBindAttribLocation после линковки, то это может привести к ошибке или
неправильной работе.
То же самое в случае с вызовом glGetAttribLocation до линковки.

В случае с glBindAttribLocation мы можем закрепить определенный атрибут под нужным


нам номером, но, всё же, лучше на мой взгляд использовать glGetAttribLocation, так как это
не приведёт к путанице.

После того, как мы получили ссылку ( индекс ) атрибута, нам нужно закрепить за атри-
бутом данные c помощью функции glVertexAttribPointer.

glVertexAttribPointer(int AtributeHandle, int Size, int Type, boolean Normalized,


int Vertex_offset, int Attr_offset, Buffer b])
glVertexAttribPointer(int AtributeHandle, int Size, int Type, boolean Normalized,
int Vertex_offset, Buffer b])

где:
AtributeHandle – ссылка на атрибут,
Size – количество ЭЛЕМЕНТОВ атрибута,
Type – тип данных атрибута для передачи ( а не тип атрибутов в шейдере).
Может принимать значения:
GL_BYTE,
GL_UNSIGNED_BYTE,
GL_SHORT,
GL_UNSIGNED_SHORT,
GL_FIXED и
GL_FLOAT.

Normalized – нормализация атрибута, в случае True беззнаковые значения скалируются


до 0 – 1.0, знаковые от -1.0 до 1.0.
Если false – передаётся по значению и приводится к типу данных в шейдере.
То есть мы передает uBite ( беззнаковый байт ) в атрибут шейдера mediump float aAttr1,
и, пускай, для одной вершины значение будет 17, тогда в шейдере мы получим 17.0;

Vertex_offset – размер данных всех атрибутов ( вершины ) в ДАННОМ МАССИВЕ в


БАЙТАХ.
То есть, например, в массиве последовательно записаны float[3] и float[2] ( для примера
вершинные и текстурные координаты ).
В java ( и си ) под андройд float занимает четыре байта, следовательно смещение будет
(4*3)+(4*2)=20 байт.
23 ПРИВЯЗКА АТРИБУТОВ МАССИВОВ. ПОДКЛЮЧЕНИЕ И ОТКЛЮЧЕНИЕ АТРИБУТОВ111

Для java под андройд ( и си ):


GL_BYTE – 1 байт,
GL_UNSIGNED_BYTE – 1 байт,
GL_SHORT – два байта,
GL_UNSIGNED_SHORT – два байта,
GL_FIXED – четыре байта,
GL_FLOAT – четыре байта.

Обращаю внимание, что от системы к системе значения могут меняться, то есть такая
размерность не гарантируется.

Attr_offset – смещение для КОНКРЕТНОГО атрибута в байтах.


Используется для массивов в памяти GPU.
Используется ТЕКУЩИЙ буфер VBO в памяти GPU ( последний подключенный
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffer_id) )
Для прошлого примера ( float[3] и float[2] )
для первого атрибута смещение будет равно 0 ( начало блока данных для вершины )
для второго атрибута смещение будет равно 4*3=12 – смещение на три float (float[3]).
Если бы были еще атрибуты в данном массиве, то
для следующего смещение было бы 4*3 + 4*2 = 20 байт ( размер float[3] + float[2] ).

Buffer b - используется для подключения ЛОКАЛЬНЫХ массивов ( ByteBuffer, FloatBuffer


и т.д. )

Далее нам нужно включить ( задействовать ) атрибут с помощью функции:


GLES20.glEnableVertexAttribArray(aHandle);

aHandle - ссылка ( индекс ) атрибута.

Для правильной работы должен быть подключен атрибут после закрепления текущего
шейдера (glUseProgram).

После того, как закончили работу ( отрисовку ) текущего массива, атрибут нужно от-
ключить:
glDisableVertexAttribArray(aHandle);

Важно:
отключение атрибутов обязательно для массивов в памяти GPU, но рекомендую отклю-
чать и для локальных массивов.

Давайте разберем подробнее подключения массивов.

Есть разница между подключением массивов в памяти приложения (локальных) и масси-


вов в памяти 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); //привыкая убирать хвосты.

mTriangleVertices.position(0) – устанавливает позицию массива на 0.


Для локальных массивов в java под андройд .position задает текущую позицию, соответ-
ственно, что-бы сделать смещение, нам нужно задать нужное значение.

GLES20.glVertexAttribPointer(aPositionHandle, 2, GLES20.GL_FLOAT, false, 16, mTriangleVertices)


– закрепляем данные за атрибутом. Vertex_offset – четые float зачения ( 2+2 ) и соответ-
ственно 16 байт, тип GLES20.GL_FLOAT, количество элементов первого атрибута – 2.

GLES20.glEnableVertexAttribArray(aPositionHandle) – разрешаем данный атрибут.

mTriangleVertices.position(2) – смещаем указатель на два float значения ( для FloatBuffer),


в случае с ByteBuffer смешение было бы 8.

GLES20.glVertexAttribPointer(aTextureHandle, 2, GLES20.GL_FLOAT, false, 16, mTriangleVertices)


– подключаем второй атрибут. GLES20.glEnableVertexAttribArray(aTextureHandle) – разре-
шаем.

и под конец убираем хвосты: GLES20.glDisableVertexAttribArray(int aHandle)

Если был третий атрибут, то смещение для него было бы mTriangleVertices.position(4),


для четвертого 4+размер третьего и т.д.
Смещение задаётся для конкретного массива, если мы подключаем с нескольких масси-
вов, то смещение задаётся для каждого отдельно.

Давайте теперь рассмотрим подключение массива в памяти GPU:

Пускай есть массив атрибутов в ссылкой id, содержащий атрибуты


Position (float[3]),
Normal (float[3]),
Texture (float[2]).
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER,id);
GLES20.glEnableVertexAttribArray(aPositionHandle);
GLES20.glEnableVertexAttribArray(aNormalHandle);
GLES20.glEnableVertexAttribArray(aTextureHandle);

GLES20.glVertexAttribPointer(aPositionHandle, 3, GLES20.GL_FLOAT, false, 4 * (3 + 3


+ 2), 0); // подключаем первый атрибут
GLES20.glVertexAttribPointer(aNormalHandle, 3, GLES20.GL_FLOAT, false, 4 * (3 + 3 +
2), 4 * 3); // подключаем второй атрибут
24 "ЗАГЛУШКИ"АТРИБУТОВ 113

GLES20.glVertexAttribPointer(aTextureHandle, 2, GLES20.GL_FLOAT, false, 4 * (3 + 3


+ 2), 4 * (3 + 3)); // подключаем третий атрибут
...
[ отрисовка примитивов для данного массива ]
...
GLES20.glDisableVertexAttribArray(aPositionHandle);
GLES20.glDisableVertexAttribArray(aNormalHandle);
GLES20.glDisableVertexAttribArray(aTextureHandle);

GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER,id) – подключаем массив


GLES20.glEnableVertexAttribArray(aHandle) – разрешаем атрибуты

GLES20.glVertexAttribPointer(aPositionHandle, 3, GLES20.GL_FLOAT, false, 4 * (3 + 3 +


2), 0);
Size – 3 ( количество элементов атрибута )
Vertex_offset – 4 * (3 + 3 + 2) = 32 байта
Attr_offset – 0, смещение первого атрибута.

Для второго атрибута смещение Attr_offset - 4 * 3


( то есть размер float * размер_первого_атрибута ).
Для третьего атрибута смещение Attr_offset - 4 * (3 + 3)
( то есть размер float * (размер_первого_атрибута + размер_второго)).

и отключаем атрибуты после отрисовки:


GLES20.glDisableVertexAttribArray(int aHandle)

Важно:
Для массивов в памяти GPU отключение ОБЯЗАТЕЛЬНО.

Важно:
Атрибуты можно подключать смешано с разных типов массивов ( локальных или в памяти
GPU).
То есть часть атрибутов может быть в памяти GPU, часть в памяти приложения и при-
надлежать разным массивам.

Вот и всё по подключению массивов атрибутов.

24 "Заглушки"атрибутов
Может возникнуть ситуация, когда требуется выставить атрибут в определенное ( един-
ственное ) значение атрибута для всего меша, так как не гарантируются значения для непод-
ключенных атрибутов, и при этом не имеет смысла расходовать память для указания одних
и тех-же значений.

Это можно реализовать с помощью функций:


void glVertexAttrib{1234}{f}( uint index, T values );
void glVertexAttrib{1234}{f}v( uint index, T values );

где:
index - ссылка на атрибут,
values - значения атрибута.
25 ПОЛУЧЕНИЕ ИНФОРМАЦИИ О ПОДКЛЮЧЕННЫХ ШЕЙДЕРАХ, УНИФОРМАХ, АТРИБУТАХ1

А вообще, нужно стремиться избавиться от возникновения таких моментов и не пытаться


сделать универсальный шейдер, где будут незадействованные атрибуты. По логике работы
glVertexAttrib дублирует функционал униформ.

25 Получение информации о подключенных шейдерах, уни-


формах, атрибутах
Для этого служат функции:
public static void glGetActiveAttrib (int program, int index, int bufsize,
IntBuffer length, IntBuffer size, IntBuffer type, byte name)

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 Отрисовка примитивов
Для отрисовки примитивов используется несколько функций в зависимости от типа мас-
сива ( массив индексов или "простой").

Давайте рассмотрим отрисовку без массива индексов:


glDrawArrays(int mode, int first, int count); // отрисовка массива примитивов

где:
mode – тип примитивов для отрисовки.
Бывает:
GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES,
GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES.
first – первая вершина
count – количество вершин для отрисовки

При вызове произойдет отрисовка ( передача на отрисовку ) элементов от first до first +


count - 1.
26 ОТРИСОВКА ПРИМИТИВОВ 115

Пример:
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, 6); // отрисует шесть вершин и соответс
твенно два полигона

Отрисовка массива индексов:


glDrawElements(int mode, int count, int type, Buffer indices); // отрисовка массива
индексов примитивов в памяти приложения
glDrawElements(int mode, int count, int type, int offset); // отрисовка массива инд
ексов примитивов в GPU

где:
mode – тип примитивов для отрисовки.
Бывает:
GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES,
GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES.

count – количество вершин для отрисовки


type – тип индексов. UNSIGNED_BYTE или UNSIGNED_SHORT.
indices – массив индексов для локального массива. Смещение от начала массива задаеться
.position() бефера;
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 116

offset – смещение для массива в памяти GPU.


Пример:
GLES20.glDrawElements(GLES20.GL_TRIANGLES,120,GLES20.GL_UNSIGNED_SHORT,0);
// отрисует 120вершин ( 40 полигонов ) от начала массива индексов в памяти GPU.

Вот и всё.

Давайте теперь рассмотрим последовательность всех действий:

При загрузке (onSurfaceCreated или onSurfaceChanged):


1. загрузить данные текстур, массивы атрибутов вершин ( меши )
2. загрузить – откомпилировать - слинковать шейдеры
3. получить ссылки на униформы/атрибуты

Далее отрисовка (onDrawFrame):


1. закрепить атрибуты за данными. подключить атрибуты.
2. установить значения униформ.
3. отрисовать примитивы
4. отключить атрибуты

Главное не компильте шейдеры и не получайте ссылки на атрибуты в потоке отрисовки


(onDrawFrame),
Всё это надо сделать на этапе загрузки или в отдельном потоке – иначе тормоза и
задержки обеспеченны.

27 Системы частиц. Билборды. Точечные спрайты


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

Спрайт – сейчас под этим понимается несколько достаточно разных значений. В пер-
воначальном смысле "спрайт" – это небольшое изображение, которое мы свободно можем
перемещать по экрану.

В данный момент то, что понимается под спрайтом, можно разделить на несколько групп:

Billboard ( билборд, по аналогии в рекламными вывесками ) – спрайт, постоянно по-


вёрнутый лицом к камере с определенным углом. Такие спрайты используются как для от-
рисовки систем частиц – дым, снег, трава и т.д., так и, например, для отрисовки интерфейса.

Decal ( декаль ) – в общем смысле средство для повышения детализации, обычно накла-
дывается поверх основной геометрии. Из примеров – граффити на стене, "дырка" в обоях,
плакат и т.д. Грамотное использование декелей позволяет увеличить детализацию и краси-
вость картинки в несколько раз при совсем небольших затратах.
Обычно "новички" пренебрегают декалями – из-аз чего их работы выглядят достаточно
печально. . .
Стандартная ошибка – например стена в повторяющейся текстурой обоев размноженное
много раз по всем осям.
Такая стена будет выглядеть печально и фальшиво.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 117

А вот если добавить пару пятен – потертостей, небольшое "оттенение" некоторых участ-
ков, "повесить" даже маленькую картину – то будет совершенно другой вид.

Impostor (Импостер) – перерендеренная в текстуру 3D модель.


В большинстве заменяет объект при удалении от него на определенное расстояние.
Обычно является частью LOD-систем.
Могут быть как предпросчетом так и динамическими.
Ещё интересный способ использования данной техники – создание "толпы".

В данном случае нас интересуют билборды.

27.1 Билборды
Под билбордом обычно понимают прямоугольник перпендикулярный камере ( взгляду ) –
лицевой стороной ориентированный на камеру.

Хотя возможны для некоторых задач варианты, когда требуется определенный угол по
отношению к камере.

В OpenGL в роли билбордов выступают обычные полигоны.


На старых версиях OpenGL ES это был единственный способ отрисовки таких спрайтов.
Билборд обычно представляет из себя прямоугольник составленный из двух полигонов и
отрисовывается как обычные полигоны за исключением того, что дополнительно разворачи-
ваем лицом к камере.

Листинг 21: Один из способов задачи билборда


uniform mat4 projection;
uniform mat4 worldView;

attribute vec4 position;


. . .
void main() {
. . .
gl_Position = projection * (position + vec4(worldView[3].xyz, 0));
. . .
}

Или можно отдельно передавать позицию камеры, и на основе её вычислять положение


полигонов, способов много и они достаточно сильно отличаются.
Лучшего способа нет – всё выбирается под задачу.

Достоинства Билбордов:
1. C ними можно делать всё что можно делать с "обычной" геометрией.

Недостатки:
1. Большая избыточность данных. Требует четыре вершины для задания каждого бил-
борда, ещё обычно добавляют центр билборда, размер.
2. Сильная нагрузка на вершинные шейдеры.
3. Большая нагрузка на CPU и шину, как следствие задачи большого числа данных.
4. Проблема множественной перерисовки при маленьком размере частиц.

Последнее давайте рассмотрим поподробней.


27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 118

Посмотрим на рисунок:

Сетка на заднем фоне изображает пикселя итогового изображения.


Как видно из рисунка, одно ребро у полигонов общее.

Так каким образом будет растаризоватся грани попавшие на общее ребро?


Такие пикселы выделены темным цветом.

На самом деле, GPU будет просчитывать эти пикселя ДВА раза для каждого полигона
и смешивать в итоговое значение. Без этого швы были бы слишком сильно заметны и "на
глаз" модели бы распадались на полигоны.

Теперь посмотрим на следующий рисунок:


27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 119

И увидим, что при уменьшении спрайта количество пикселей процентно увеличилось.

А сколько раз отрисуется точка, когда спрайт уменьшится до одного пикселя?

Вообще это общая проблема всей геометрии – и представьте, что происходит, когда
моделька в несколько тысяч полигонов уменьшается до нескольких пикселов. . .
На экране в итоге мы видим несколько десятков пикселей – а просчитаны были тысячи.
Большинство мобильных GPU подвержены такому неприятному эффекту.
Спасает только LOD – да и то не всегда.

Овердроу – неприятный и вредный эффект получающийся, когда мы отрисовываем одну


и ту же точку несколько раз.
То есть при неправильной сортировке – например, рисуем небо на весь экран, следом
землю и следом деревья и получаем
в итоге, что точка которая "выпала" на дерево отрисовалась несколько раз – сначала небо,
потом земля и только потом дерево.
( некоторые GPU избавлены от такого недостатка но полагаться на это не стоит )
По возможности стоит избавляться от узких полигонов – но не всегда это возможно.

Например в данном случае лишняя отрисовка при удалении от "столба" будет процентов
как минимум 70% из-за наличия большого количества "узких" полигонов.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 120

Для маленьких спрайтов всего этого, как и других проблем билбордов, можно избежать
задействовав точечные спрайты.

27.2 Точечные спрайты


Точечные спрайты являлись в старых версиях OpenGL ES отдельным расширением и
только начиная с OpenGL ES 2.0 входят в стандарт.

Так что же представляют из себя точечные спрайты?

Точечные спрайты ( Point Sprites ) – или как иногда называют – микрополигоны являют-
ся одним из примитивов которые может рендерить GPU на OpenGL.

Вспомним, какие есть примитивы: треугольники, линии и точки.

Вот, как раз, точки ( GL_POINTS ) и являются точечными спрайтами.

Точки задаются одной позицией ("вершиной") – центром спрайта и имеют размер.

Размер точки задается в пикселах – т.е. в конечном размере, как она будет выглядеть на
экране, поэтому, чтобы получить одну и туже картинку на разных устройствах, нужно это
учитывать.

Важно:
Точечный спрайт ВСЕГДА квадратный и ВСЕГДА расположен параллельно осям экрана
– мы НЕ МОЖЕМ поворачивать микрополигоны.

Точечные спрайты ограничены в размере – и насколько большой не задавай – больше


аппаратного ограничения не нарисует.

Узнать максимальный и минимальный размер можно с помощью:


float[] psize = new float [2];
GLES20.glGetFloatv(GLES20.GL_ALIASED_POINT_SIZE_RANGE, psize,0);

где:
первый параметр будет отвечать за минимальный размер,
второй – за максимальный.
Размеры в пикселах.

Для точки мы не передаём текстурные координаты – так как задается она одной точкой
– но в фрагментном шейдере есть специальные входящие параметры с текстурными коорди-
натами, которые генерируются автоматически.

Специальные переменные GLSL для точечных спрайтов:

Вершиный шейдер:
gl_PointSize – исходящий параметр, задает размер РАСТАРИЗОВАННОЙ точки.

Фрагментный шейдер:
gl_PointCoord – сгенерированные текстурные координаты для спрайта.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 121

Рассмотрим простой шейдер:


[vertex]

attribute vec2 aVector; // направление движения спрайта


attribute float aSpeed; // скорость движения спрайта
attribute float aSize; // размер спрайта
attribute vec3 aColor; // цвет

uniform float uTime; // время

varying vec4 vColor;

void main()
{
vColor = vec4(aColor,1.0);

// если спрайт вышел за границу экрана переносим на начало


gl_Position.xy = mod((aVector*(aSpeed*uTime) + aPositionStart),2.0)-1.0;
gl_Position.zw = vec2(1.0,1.0);

gl_PointSize = aSize;

[fragment]

precision mediump float;


varying vec4 vColor;
uniform sampler2D t_texture;

void main()
{
gl_FragColor = vColor*texture2D(t_texture, gl_PointCoord);
}

Этот шейдер может быть использован для построения ПРОЦЕДУРНОЙ системы частиц.
т.е. раз задав значения и меняя только один или несколько параметров, приводим всю
систему в движение.

aVector – в данном случае 2D вектор задающий направление ( для 3D соответственно 3D


вектор ).
Вектор нормализован для удобства вычислений.

aSize – размер спрайта в пикселах.


Размер я вынес в атрибуты для того, чтобы можно было задавать разные размеры для
каждого спрайта.

aSpeed – скорость движения.

"aVector*(aSpeed*uTime) + aPositionStart" задает текущею позицию спрайта.


Вектор направления умножаем на время умноженное на скорость и прибавляем старто-
вую позицию.

"mod(. . . ,2.0) – 1.0" – остаток от деления на 2 со смещением в единицу.


Переносит частицу на начало координат по осям, когда она достигает края.
Не учитывает размер частицы – перенос осуществляется когда центр достигает края
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 122

экрана.
Для исправления этого достаточно чуть увеличить "рамки"
– например mod(. . ., 2.1) – 1.05

Всё вместе:
"gl_Position.xy = mod((aVector*aSpeed*uTime + aPositionStart),2.0) – 1.0;"

aVector*aSpeed можно вычислить на этапе создания массива атрибутов – но для нагляд-


ности вынес в отдельное значение.

Атрибуты из данного примера в реальной задачи лучше упаковать в два атрибута – так
как использовать отдельно несколько float-атрибутов и vec2 нерационально.

"gl_PointSize = aSize;" – задаем размер спрайта, в пикселах.

В фрагментном шейдере получаем цвет текстуры по координатам gl_PointCoord ( автома-


тически сгенерированным ) и умножаем на цвет:
"gl_FragColor = vColor*texture2D(t_texture, gl_PointCoord);"

Главное достоинство такой системы частиц – то, что мы задаем начальные данные только
на этапе инициализации – а дальше
спокойно можем передать их в память GPU и анимировать всю систему на несколько
тысяч частиц
передачей нескольких параметров через униформы – в данном случае время.

Количество частиц удобней регулировать частичной отрисовкой такого массива.


Например, нам нужно регулировать силу дождя – сразу генерируем массив, например, на
три тысачи частиц и отрисовываем нужное количество, когда нам понадобиться – например
при слабом дожде 300 частиц, при сильном – 3000.

Пересоздавать массив каждый раз очень не рационально.

Можно добавить вектор ветра и любые другие значения, которые будут влиять на систе-
му и направление движения частиц.

На мобильных устройствах в данный момент очень много памяти – и экономия несколь-


ких килобайт нас не спасет – но
при этом мало мощностей и узкая шина памяти – так что такой вариант идеально подхо-
дит.

Давайте рассмотрим другой вариант построения системы частиц – дым из трубы парово-
за ( или от спички ) которая двигается.

В этом случае мы не знаем заранее где эмиттер ( излучатель частиц, место где частицы
появляются ) – так что все просчитать заранее не получится.

Но всё равно у нас есть куда данных которые можно обработать процедурно.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 123

Пускай у каждой частицы есть:


0. позиция ( 3-float )
1. вектор движения ( 3-float )
2. скорость движения ( 1-float )
3. направление ветра ( 3-float )
4. размер частиц – в начале и конце жизни ( 2-float )
5. продолжительность жизни частиц ( 1-float )
6. изменение цвета ( например прозрачности ) во время жизни. ( 4-float )

Теперь давайте посчитаем сколько нам может понадобиться частиц ( по максимуму ):


пускай каждый кадр появляется 3 новых частицы.
продолжительность жизни частиц – от 5 до 7 секунд.
частота кадров – 60fps.

Итого максимальное количество возможных живых частиц – 60*7*3 = 1260.

Теперь представим, что будет, если все параметры каждой частицы мы будем обновлять
каждый кадр на CPU...
А если таких систем еще несколько?
Слайд-шоу. Что и было бы на ранних версиях OpenGL.

Но зачем нам обновлять все значения каждый кадр?

На самом деле, каждый кадр достаточно будет обновить параметры только ТРЕХ частиц
– а остальные спокойно посчитаются процедурно.

Первым делом генерируем массив на 1260 спрайтов и зададим только позицию выходя-
щую далеко за экран.

Далее каждый кадр ( или через один например =) ) будем обновлять атрибуты трех
частиц, т.е. задавать:
Начальную позицию, вектор дальнейшего движения, размер, продолжительность жизни
и т.д.

При этом введём еще один атрибут – время смерти.


Время смерти будет равно текущее время + время жизни.
Теперь шейдер:
. . .
attribute float aTimeOfDeath;
uniform float time;

main(){
. . .
If(aTimeOfDeath<=time) {
gl_Position = vec(1000.0,1000.0,1.0,1.0); // отправляем спрайт к черту на куличи
ки
}else{
// спрайт жив. Рассчитываем все как обычно.
. . .
}
. . .
}
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 124

Как это будет работать?

Когда "дата смерти" спрайта будет меньше текущей даты – мы точно уверены, что спрайт
мертв – и отправляем его по координатам далеко за экран.
По таким координатам он не будет растерезовыватся на 100%.
Соответственно таким способом отсеиваются мертвые спрайты – причем очень "дешево"
в плане вычислений.

Все данные одного спрайта мы рассчитываем/задаем только ОДИН раз – дальше все
считаем процедурно.
Получается хорошая экономия мощностей.

Когда мы дойдём до конца массива – 1260 позиции, мы уже будем уверены, что спрайты
под первыми позициями "умерли" и переносим указатель на начало массива.

Вот и вся отрисовка – очень быстро и компактно.

п.с. никто не мешает отрисовывать хоть десять источников из одного массива – тогда
таким способом ещё можно регулировать количество спрайтов одновременно на экране –
при нехватке "свободных" будут затираться самые старые,
что в глаза не сильно будет бросятся – но будет лучше в плане стабильности fps.

Таким же способом легко изобразить стрелялку состоящую из 100500 пулек на экране да


и любые системы частиц с перемещающимися эмиттерами - пар, дым, огонь, взрывы, пули,
воду и т.д.

А если попытаться считать всё это по старинке – ничего кроме тормозов не получим.
И даже если мощностей CPU хватит GPU все равно будет справляться с этой задачей
эффективней хотя бы в плане затрат аккумулятора – а уж поверьте, пользователям есть
разница посадит игра/приложение телефон за полтора часа или за пять.

Листинг 22: Пример шейдера для системы частиц с эмиттером


[vertex]

attribute vec2 aPositionStart;


attribute vec2 aVector;
attribute vec2 aSize;
attribute vec4 aColorStart;
attribute vec4 aColorEnd;
attribute vec2 aLifeTime;

uniform float uTime;

varying vec4 vColor;

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

vColor = mix(aColorStart,aColorEnd,delta); // микшуем цвета

Position = aVector*(aLifeTime.x*delta) + aPositionStart;


gl_PointSize = mix(aSize.x,aSize.y,delta);
}
gl_Position.xy = Position;
gl_Position.zw = vec2(1.0,1.0);
}

[fragment]

precision mediump float;


varying vec4 vColor;
uniform sampler2D t_texture;
void main()
{
gl_FragColor = vColor*texture2D(t_texture, gl_PointCoord);
}

Пример системы частиц с "динамическим" эмиттером:

(ссылок на исходники нет)

Проект сделан в последней на нынешней момент AndroidStudio ( 0.2.10 ), хотя не должен


вызвать проблем при переносе в другие IDE.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 126

Для перемещения эмиттера используете касание экрана, от 1 до 3 касаний, каждый эмит-


тер привязан к одному касанию.

В проекте используется частичная замена данных в буфере атрибутов GPU и из-за про-
блем с буферами ( ошибки ) в версиях Android меньше чем 2.3 минимальная версия API
9.
Для того, чтобы запустить на 2.2 нужен внешний ( или исправленный ) парсер OpenGL.

Давайте соберем достоинства и недостатки использования точечных спрайтов (микропо-


лигонов)

Достоинства:
1. Нет проблемы “овердроу” при маленьком размере спрайта почти на всех GPU.
2. Требуется только обработка одной вершины и не требует дополнительных операций
для поворота к "зрителю", что создает в несколько раз (!) меньшую нагрузку на вершинные
блоки GPU.
3. Позволяет просто создавать большие динамические системы без кучи избыточных
данных – что позволяет экономить память и как самое главное – пропускную шины данных.
4. На большинстве GPU быстрее растеризуются чем обычные полигоны.
5. текстурные координаты генерируются автоматически. Нет необходимости в дополни-
тельных атрибутах и обработках.

Недостатки:
1. Имеют ограниченный размер. Хотя как минимум 64 пикселя.
2. Всегда квадратны. Никаких прямоугольников и т.д.
3. Грани спрайта ВСЕГДА параллельны сторонам экрана – т.е. невозможно повернуть
частицу.
4. Размер спрайта привязан к пикселям экрана.
5. текстурные координаты генерируются автоматически ( пункт не ошибка =) ) что не
подходит для некоторых задач.

Давайте рассмотрим пункт 5


( из обоих списков ) подробнее.

Что представляют из себя текстурные координаты? – обычную точку в 2D.


Что мы можем делать с точкой в 2D? – все преобразования – поворот, перенос, скалиро-
вание и т.д.

Мы не можем передать готовые координаты текстуры в фрагментный шейдер напрямую


– но можем преобразовывать стандартные координаты.

Например, нам нужно выбрать спрайт из атласа 5x5 ( в атласе спрайты распределены
равномерно ), пускай координаты спрайта [1,2].

Мы знаем, что автоматически генерируемые текстурные координаты от 0.0 до 1.0 по


каждой оси – следовательно для того, чтобы получить нужные нам координаты нужно их
разделить на размер атласа и прибавить смещение спрайта делённое на размер атласа.

Для удобства будем не делить – а умножать.


27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 127

Сначала вычислим обратное размеру атласа:


vec2 AtlasRes = 1.0/vec2(5.0,5.0);
// думаю понятно что это лучше вынести в униформу.

В данном случае получится 0.20 по каждой оси.

Далее нам нужно вычислить смещение координат атласа:


vec3 SpriteTr = uAtlasRes*aSprite;

Где vec2 aSprite = 1.0,2.0 ;

Далее передаем AtlasRes и SpriteTr в фрагментный шейдер и вычисляем новые координа-


ты:
vec2 NewCoord = gl_PointCoord*vAtlasRes+SpriteTr;

То есть скалируем текстурные координаты по размеру атласа и прибавляем смещение.

Внимание:
Давайте обратим внимание на строчку gl_PointCoord*vAtlasRes+SpriteTr;

За сколько операций выполнит эту строчку современный GPU?


За одну операцию/цикл.

Такая операция называется MAD - MUL+ADD


Умножение плюс сложение – и выполняется за один такт для четырехмерных векторов и
ниже.
Компиляторы должны сами оптимизировать такие вещи – но как показала практика они
делают это не всегда.
Поэтому старайтесь вручную приводить вычисления к такому виду.
Да и на многих GPU умножение быстрее, чем деление – лучше умножать на обратное (
по возможности ), чем делить.

Пример шейдера
[vertex]

attribute vec2 aVector;


attribute float aSpeed;
attribute float aDeltaPos;
attribute float aDeltaSize;
attribute vec2 aSprite;
uniform float uTime;
uniform vec2 uAtlasRes;
uniform vec2 uStartPosition;
uniform vec4 uColorStart;
uniform vec4 uColorEnd;
varying vec4 vColor;
varying vec2 vSprite; // координаты спрайта ( в спрайтах =) )
varying vec2 vAtlasRes; // обратное размеру атласа

void main()
{
float absTime = fract(aDeltaPos+uTime);
vColor = mix(uColorStart,uColorEnd,absTime);
vAtlasRes = uAtlasRes; // передаем обратное размеру атласа
vSprite = uAtlasRes*aSprite; // вычисляем смещение
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 128

vec2 deltaPos = aVector*aSpeed*absTime; // вычисляем текущую позицию


gl_Position.xy = uStartPosition+deltaPos;
gl_Position.zw = vec2(1.0,1.0);
gl_PointSize = 30.0 + aDeltaSize;
}

[fragment]
precision mediump float;

varying vec4 vColor;


varying vec2 vSprite;
varying vec2 vAtlasRes;
uniform sampler2D t_texture;

void main()
{
gl_FragColor = texture2D(t_texture, (gl_PointCoord*vAtlasRes+vSprite))*vColor;
}

А как быть с поворотом и другими операциями? – лучше использовать матрицу транс-


формации.
Таким способом можно задать любые трансформации текстурных координат.
Для 2D вектора будет достаточно матрицы 3x3.

Например, для поворота вокруг центра текстуры нам нужно перенести координаты сере-
дины ( [0.5,0.5] ) в нулевую точку – затем повернуть – и перенести обратно.
Таким трансформациям соответствует следующая матрица
float CosA = cos(angle);
float SinA = sin(angle);

mat3 TexMat = mat3(


CosA, -SinA, (-0.5*CosA+0.5*SinA+0.5),
SinA, CosA, (-0.5*SinA-0.5*CosA+0.5),
0, 0, 1
);

Я думаю, понятно, что вычислять такую матрицу нужно в вершинном шейдере и переда-
вать в фрагментный уже готовой.

Далее умножаем координаты на матрицу и получаем поворот текстуры:


gl_FragColor = texture2D(t_texture, (vec3(gl_PointCoord,1.0)*vTexMat).xy);

Умножение на матрицу занимает как максимум 3 такта\цикла, хотя некоторые GPU мо-
гут делать такие операции и за один цикл.

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

Хотя к слову один и три цикла – не очень сильная нагрузка.

Если умножить операции в вершинном шейдере на три


+ добавить операции поворота * 4 . . .
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 129

Пример с тремя системами спрайтов:


1. простая система
2. система в спрайтов из атласа
3. система с поворотом текстуры

(исходников нет)

Проект сделан в последней на нынешней момент AndroidStudio ( 0.2.10 ), хотя не должен


вызвать проблем при переносе в другие IDE.

Так что же использовать в своем проекте? Билборды или точечные спрайты?

Если спрайты достаточно большие, больше нескольких десятков единиц по каждой оси и
требуют сложных трансформаций или плохо вписываются в квадрат, то лучше использовать
обычные билборды ( полигоны ) – так как все плюсы использования точечных спрайтов
пропадут в этом случае.

Если маленькие частицы в большом количестве, не требующие хитрых трансформаций –


то тут однозначно лучше точечные спрайты.
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 130

Например, понятно, что длинный и тонкий объект не очень хорошо вписывать в квадрат,
и, используя в данном случае ( рис. ) квадратные микрополигоны, мы потеряем кучу ресур-
сов впустую:

Всегда нужно смотреть на конкретную задачу.

Половина чипов несимметричны ( Tegra, Mali ) и число блоков в них обычно от 1к4 до
1к2 - вершиных к фрагиментным.
И вполне может возникнуть ситуация, когда вершинные блоки перегружены – а фраг-
ментные простаивают.

Поэтому выбирать какие блоки лучше и как нагружать нужно под конкретную задачу.

Невозможно написать эффективную УНИВЕРСАЛЬНУЮ БЫСТРУЮ систему частиц


под все задачи.
Это будет - избыточность данных, лишние операции, нагрузка не на "малозагруженные"
вычислительные блоки и нехватка функциональности под конкретную задачу.

Хотя никто не мешает нам использовать смешанную систему – точечные спрайты впере-
мешку с билбордами используя сильные стороны обоих подходов.

Ещё бы хотел предупредить о опасности множественного смешивания – блендинг создает


огромную нагрузку на GPU.

Двести полупрозрачных спрайтов не пересекающихся на экране и двести спрайтов отри-


сованных в одной точке создадут разную нагрузку на GPU – и намного тяжелее отрисован-
ные в одной точке.

Многим кажется ( почему-то ), что прозрачные точки совсем ничего не весят – хотя на
самом деле они тяжелее в вычислении чем непрозрачные пикселя.

Вот и всё по точечным спрайтам.

( текст еще буду вычищать, выложил как есть пока - а то это может длится долго. )
27 СИСТЕМЫ ЧАСТИЦ. БИЛБОРДЫ. ТОЧЕЧНЫЕ СПРАЙТЫ 131

Информация взята с топика на форуме

Исходники качаем оттуда

Спасибо за информацию usnavii

Верстальщик

С предложениями на почту: who-e@yandex.ru

Вам также может понравиться