GrabDuck

OpenGL 2.0+ под Android - 4PDA

:

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

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

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

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

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

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

Для полигона ( треугольника ):
Вершинный шейдер отрабатывает три раза - для каждой вершины. Далее запускается фрагментный шейдер для каждого пикселя при растаризации данного полигона.

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

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

---------------------------------------------------------

Приступим.

Давайте рассмотрим работу простейшего шейдера:

Код:

[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.0, // первая вершина
0.0,0.75,0.0, // вторая
0.75f, -0.75f,0.0}; // третья

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

Что это за массив?
Рисунок:

Прикрепленное изображение

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

Важно: OpenGLES 2.0 может отрисовывать примитивы только заданные массивом или массивами атрибутов вершин. Другим способом задание примитивов невозможно.

Рисуется один полигон.

Что получим после отработки шейдера:
Прикрепленное изображение
получаем полигон закрашенный белым.
( конечно без стрелочек и точек )

Последовательность выполнения шейдера:

Весь код шейдера выполняется для каждой вершины отдельно.
Значение переменной aPosition будет индивидуальным для каждой вершины и будет браться из массива posattr].

Важно:
Как видите данные положения вершины в массиве представляют собой трехкомпонентный вектор,
а aPosition четырехмерный. Тут есть тонкость. Для умножения координат вершины на матрицу 4x4 нужен четырехкомпонентный вектор (x,y,z,1.0) и специальная переменная gl_Position тоже использует четыре компонента. Последняя хитрая координата называется 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 не может – поэтому не рисуем заведомо невидимые объекты, так как для них все равно будет отрабатывать вершинный шейдер.

Важно: Существует главное ограничение на работу всех шейдеров:
откуда считываем – писать нельзя, куда пишем – читать нельзя.
То есть нельзя например рендерить в текстуру и читать из нее одновременно.
Приведет от глюков до падения драйверов.
второе ограничение - отсутствие какой либо связи между ядрами GPU.
Из чего следует что мы не можем определить что ”насчитали” соседние блоки.

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

Далее идет GPU преобразовывает данные к экранным координатам и начинает растеризацию. То есть просчитывает отдельные пиксели.

Для каждого пикселя отдельно выполняется код фрагментного шейдера.

В нашем случае:

Код

gl_FragColor = vec4(1.0);

что соответствует gl_FragColor = vec4(1.0,1.0,1.0,1.0);

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

Напомню что специальная переменная gl_FragColor которую условно можно считать ”возвращаемым значением” фрагментного шейдера,
в ней возвращается значение цвета пикселя в формате RGBA.

от и все что делает данная шейдерная программа.
--------------------------------------------------------------------------------------------------------------

Давайте теперь разберем что такое тип 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}; // третья

Два массива можно записать как один

Код

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

Как и в прошлом примере рисуем один полигон.

Код:

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

}

Запускаем и получаем такую картинку:
Прикрепленное изображение

Как видите мы получили линейную интерполяцию.

Как это работает:
Перед каждым выполнением фрагментного шейдера GPU вычисляет интерполяцию значений вершин для положения пикселя T по формуле:

D = d1+d2 … +dN

w1 = (1 - d1/D)/(N-1)
w2 = (1 - d2/D)/(N-1)

wN = (1 - dN/D)/(N-1)

vT = w1*v1 + w2*v2 … + wN*vN

Где N – количество вершин примитива

d1,d2,dN – расстояние между вершиной и точкой T.
vT – позиция вершины.

по этой же формуле будут линейно интерполироваться все данные переданные через varying-переменные из вершинного в фрагментный шейдер.

Такие как текстурные координаты ( нам же нужны координаты в конкретной точке полигона а не координаты вершины ), нормали полигонов, цвет вершин и т.д.

Проще говоря – в varying-переменную фрагментного шейдера попадает интерполированное значение этой переменной со всех вершин примитива для конкретного пикселя.

Рисунок:
Прикрепленное изображение

Все делается аппаратно на GPU и программист никак не может повлиять на это.

Минимальное количество поддерживаемых varying-переменных по стандарту – 8 четырехмерных векторов.

Если мы хотим передать определенное значение без интерполяции – то его придется задать для всех вершин примитива.

-----------------------------------------------------------------------------------------------------------------------------------

Теперь рассмотрим другой тип переменных - униформы ( 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} // сдвиг

Код шейдера:

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

}

Получим такую картинку ( желтый полигон ):
Прикрепленное изображение

Как это работает:

У каждой вершины меняем координаты, прибавляя 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-переменные.

-------------------------------------------------------------------------------------------------------------

Пример: Трансформации примитивов в 2D.

Посмотрим на рисунок:
Прикрепленное изображение

Абсолютные координаты OpenGL ES от -1 до 1 по каждой оси !вне зависимости от пропорций экрана!.

Координата 0,0 приходится на центр.

Связь с координатами Android:
Важно: координата Y с системе Android перевернута по отношению к абсолютным координатам OpenGL, то есть идет не снизу вверх а сверху вниз.

Перевод координат Android в абсолютные OpenGL можно перевести следующим образом:
пускай aX,aY – точка в координатах Android.
screenW и screenH – ширина и высота сурфейса( GLsurface ) в пикселях.

Тогда абсолютные координаты OpenGL будут:

X = (2.0/screenW) * aX – 1.0
Y = 1.0 – ((2.0/screenH) * aY)

Пускай размеры экрана 800x480

Нам нужно найти координаты 400,240 ( середина экрана )

Получаем:
X = (2.0/800) * 400 – 1.0 // = 0.0
Y = 1.0 - ((2.0/480) * 240) // = 0.0
То есть получили центр координат 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.
При обходе вершин против часовой стрелки по умолчанию считается, что полигон направлен лицевой стороной на нас.

Трансформации:

Нас интересуют три трансформации:

Translate – параллельный перенос
Scale – масштабирование ( скалирование )
Rotate – поворот

Рассмотрим Translate:

Пускай перенос задан двумерным вектором vTranslate(0.25,0.25)

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

v’ = v + vTranslate;

Что можно расписать как

v’.x = v.x + vTranslate.x;
v’.y = v.y + vTranslate.y;

Где v’ – новые координаты вершины, v – вершина, vTranslate- вектор переноса.

Посмотрим что получилось:
Прикрепленное изображение
Как видите v1 перенеслась по координатам (-0.5+0.25,-0.5+0.25) = v’(-0.25,-0.25)

Рассмотрим Scale:

Для того чтобы проскалировать нужно умножить вектор вершины на вектор скалирования.

Пускай скалирование ( масштабирование ) заданно двумерным вектором vScale(0.5,0.5)

Тогда v’ = v * vScale;

Что получилось:
Прикрепленное изображение

То есть v1’ = v1 * vec2(0.5,0.5) = v1’(-0.25,-0.25)

Рассмотрим Rotate:

Для того чтобы повернуть вершину ( вектор ) нужно:

x' = x*cos(t) - y*sin(t)
y' = x*sin(t) + y*cos(t)

Где t – угол поворота в радианах.

Поворот происходит относительно начала координат!

Поворот происходит против часовой стрелки.

Пускай хотим повернуть меш на 45градусов, что соответствует Pi/4 в радианах.

v.x' = v.x*cos(t) – v.y*sin(t)
v.y' = v.x*sin(t) + v.y*cos(t)

Рисунок:
Прикрепленное изображение

А что делать если мы хотим повернуть во круг точки отличной от начала координат?

Для этого нам сначала нужно сделать перенос а уже потом поворот.

Например мы хотим повернуть относительно точки v1 на 45 градусов.

Для этого нужно чтобы координаты вершины v1 были равны 0,0 – соответственно весь меш нужно перенести на координаты обратные v1.

v1’ = v1-v1
v2’ = v2-v1
v3’ = v3-v1
v4’ = v4-v1
...

Далее сделать поворот.

Рисунок:
Прикрепленное изображение
Далее нужно вернуть начальные координаты меша

v1’ = v1+v1''
v2’ = v2+v1’’
v3’ = v3+v1’’
v4’ = v4+v1’’

Где v1’’ – первоначальные координаты вершины v1

Вот что получилось:
Прикрепленное изображение

Важно:
Как видите для трансформаций важна последовательность действий.

Для поворота, скалирования и переноса меша вокруг свое оси последовательность действий такая:

1. поворачиваем
2. cкалируем
3. переносим

пример шейдера для простой трансформации в 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} // перенос, двумерный вектор

Код шейдера:

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

}

Результат:
Прикрепленное изображение

Как это работает:

Объявляем двумерный вектор и поворачиваем вершину.

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

Можно заменить на одну строчку:

uniform mat4 uVertMatrix;
...
gl_Position = u_VertMatrix * 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.

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

---------------------------------------------------------------------------------------

Пример: Текстуры.

Нарисуем текстурированный спрайт в 2D ( вернее с Z-координатой равной 0.0 )

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

Атрибуты вершин:

Координаты вершины, 2 float-а - a_vertex.
текстурные координаты, 2 float-a - a_texcoord.

Записаны в массиве:

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 вершин.

Код шейдера:

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

Получаем что-то вида:
Прикрепленное изображение

Как это работает:

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:

Это нужно для того чтобы GPU интерполировал текстурные координаты между вершинами и мы на входе фрагментного шейдера получили координаты для конкретной точки.

Далее:

Фрагментный шейдер состоит из одной строчки:

gl_FragColor = texture2D(t_texture1, v_texcoord);

Значения текстуры по заданным текстурным координатам получаем с помощью функции texture2D(sampler s,vec2 с), первый параметр которой sampler2D задающий текстурный слот и второй параметр двумерные координаты текселя на текстуре. Возможен третий параметр, он задает уровень лода ( мипмапа ) для конкретной операции. Если его не указать то уровень детализации берется автоматически ( чаще лучше не трогать =).
sampler2D на самом деле это целое число, нумерация слотов идет с 0 и точно до 31, то есть передавая из нашей программы в шейдер sampler2D равный двум мы указываем на GL_TEXTURE2 )
Операция чтения текселя по другому называется текстурной выборкой.
Многие GPU умеют делать несколько выборок одновременно для каждого шейдерного процессора за такт.

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

-----------------------------------------------------------------------------------------------------
Пример: несколько текстур, смешивание.

Входные данные как и в предыдущем примере, но добавим еще несколько текстур.
Далее по тексту буду называть ссылки sampler2D не семплерами а текстурами.

Пускай есть RGBA текстуры t_texture1, t_texture2.
Пускай есть одноканальная текстура t_mix1, данные на канале r.

Текстуры используют одинаковые координаты v_texcoord.

Код шейдера:

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

}


Что получаем:
Прикрепленное изображение

Как это работает:

Строчками
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.

Пример: анимация текстуры.

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

Атрибуты вершин:

Координаты вершины, 2 float-а - a_vertex.
текстурные координаты, 2 float-a - a_texcoord.

Записаны в массиве:

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.

Двумерный вектор сдвига ( анимации ) текстуры - u_AnimVector.

Время - u_Time

Пускай рисуем спрайт, состоящий из двух полигонов.
Соответственно обрабатываем 6 вершин.

Код шейдера:

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

Посмотреть как это работает можно в уроке №4 на примере земли ogl4.apk
В общем случае наша задача просто смещать текстурные координаты.

Я специально не вынес u_AnimVector*u_Time из шейдера в программу, так как вместо u_AnimVector можно использовать вершиный атрибут для того чтобы текстура перемещалась неравномерно по разным вершинам, таким способом часто делают воду в ручьях или речках ( например в Skyrim ).
Или поток лавы будет выглядеть красиво при неравномерности смещения текстуры.

Сообщение отредактировал usnavii - 19.05.13, 10:39