3 октября 2012 г.

OpenGL ES 2.0. Урок первый-Шейдеры

Введение в шейдеры.
OpenGL ES 2.0 использует шейдеры языка GLSL. Шейдеры бывают двух типов - вершинный и фрагментный. В вершинном шейдере производятся расчеты над вершинами, а в фрагментном - над пикселями. Рассмотрим простой вершинный шейдер:
uniform mat4 u_modelViewProjectionMatrix;
attribute vec3 a_vertex;
attribute vec3 a_normal;
attribute vec4 a_color;
varying vec3 v_vertex;
varying vec3 v_normal;
varying vec4 v_color;
void main() {
         v_vertex=a_vertex;
         vec3 n_normal=normalize(a_normal);
         v_normal=n_normal;
         v_color=a_color;
        gl_Position = u_modelViewProjectionMatrix * vec4(a_vertex,1.0);
и соответствующий ему фрагментный шейдер:
precision mediump float;
varying vec3 v_vertex;
varying vec3 v_normal;
varying vec4 v_color;
void main() {
        vec3 n_normal=normalize(v_normal);
        gl_FragColor = v_color;
}
Теперь разберем по косточкам, какие процессы происходят в этих шейдерах.


Рассмотрим строчку uniform mat4 u_modelViewProjectionMatrix;
Через униформы (uniform) в шейдеры передаются внешние данные, которые могут быть использованы для расчетов, но не могут быть перезаписаны. Т.е. для обоих шейдеров униформы могут быть использованы только для чтения. Униформы могут быть переданы как в вершинный, так и в фрагментный шейдеры. В нашем случае униформа одна - это матрица модели-вида-проекции u_modelViewProjectionMatrix и передается она в вершинный шейдер. Ключевое слово mat4 означает, что это матрица размером 4х4 состоящая из чисел с плавающей точкой. Униформы никак не связаны с конкретной вершиной и являются глобальными константами. Например, в качестве униформ можно передать в шейдер координаты  источника света и координаты глаза (камеры). В дальнейшем для  удобства будем обозначать униформы с префиксом u_.
Атрибуты (attribute) - это свойство вершины. У вершины могут быть различные атрибуты. Например, координаты положения в пространстве, координаты вектора нормали, цвет. Кроме того, вы можете передавать в вершинный шейдер какие-либо свои атрибуты. Важно понять, что атрибут-это свойство вершины и поэтому он должен быть задан для каждой вершины. Атрибуты передаются в только вершинный шейдер. Атрибуты доступны вершинному шейдеру только для чтения и не могут быть перезаписаны. Нельзя определять атрибуты в фрагментном шейдере. В дальнейшем для удобства будем обозначать атрибуты с префиксом a_. Итак, определим в вершинном шейдере три атрибута:
координаты вершины в пространстве
attribute vec3 a_vertex;
координаты вектора нормали
attribute vec3 a_normal;
цвет вершины
attribute vec4 a_color;
Ключевое слово vec3 означает, что атрибут является вектором с тремя координатами. У вершины три координаты в пространстве X,Y,Z. У нормали три проекции на оси координат Nx,Ny,Nz. У цвета вершины четыре компоненты - красный, зеленый, синий и альфа, поэтому мы определили цвет как четырехкомпонентный вектор vec4. Важно усвоить, что вершинный шейдер обрабатывает каждую вершину отдельно и не имеет доступа к соседним вершинам. Поэтому вычислить вектор нормали в вершинном шейдере не получится, т.к. для вычисления нормали нужны минимум три вершины. Поэтому нужно рассчитывать нормали на CPU и передавать их в вершинный шейдер в качестве атрибута.
Переменные (varying) - это данные которые при переходе из вершинного во фрагментный шейдер будут вычислены для каждого пикселя путем усреднения данных вершин. Поясню подробнее. В вершинном шейдере мы имеем дело с координатами конкретной вершины. Если передать координаты этой вершины в фрагментый шейдер как varying, то на входе фрагментного шейдера получим координаты в пространстве уже для каждого пикселя, которые будут получены путем усреднения координат вершин. Процесс усреднения называют интерполяцией. Аналогично интерполируются координаты вектора нормали и координаты вектора цвета. Важно, что varying-переменные должны быть обязательно объявлены одинаково в вершинном и фрагментном шейдерах. В дальнейшем для  удобства будем обозначать varying-переменные с префиксом v_. Объявим три varying-переменные для пространственных координат, вектора нормали и цвета:
varying vec3 v_vertex;
varying vec3 v_normal;
varying vec4 v_color;
и отравим их на интерполяцию в функции main:
void main() {
         v_vertex=a_vertex;
         vec3 n_normal=normalize(a_normal);
         v_normal=n_normal;
         v_color=a_color;
        .......
}
Рассмотрим код функции main подробнее:
v_vertex=a_vertex;
Просто копируем атрибут пространственных координат вершины в varying-переменную. В фрагментном шейдере получим значение пространственных координат v_vertex для каждого пикселя.

vec3 n_normal=normalize(a_normal);
В вершинный шейдер мы передаем атрибут вектора нормали a_normal, который не обязательно должен быть нормализованным, поэтому произведем его нормализацию в промежуточный вектор n_normal, который затем отправим на интерполяцию:
 v_normal=n_normal;
В фрагментном шейдере получим значение координат вектора нормали  v_normal для каждого пикселя.

v_color=a_color;
Копируем атрибут цвета вершины в varying. В фрагментном шейдере получим значение цвета v_color для каждого пикселя.

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

 Завершает наш вершинный шейдер строка:
 gl_Position = u_modelViewProjectionMatrix * vec4(a_vertex,1.0);
Системная переменная gl_Position - это четырех-компонентный вектор, определяющий координаты вершины, спроецированные на плоскость экрана. Переменная gl_Position обязательно должна быть  определена в вершинном шейдере, иначе на экране мы ничего не увидим. Сначала преобразуем трехмерный вектор координат вершин в четырехмерный vec4(a_vertex,1.0) с добавлением четвертой компоненты = 1.0, затем помножим униформу мартицы модели-вида-проекции на этот вектор и получим координаты вершины на экране gl_Position.

Приступим к рассмотрению фрагментного шейдера.
void main() {
        vec3 n_normal=normalize(v_normal);
        gl_FragColor = v_color;
}
Напомню, что varying-переменные поступают в фрагментный шейдер для каждого пикселя в интерполированном в виде. В процессе интерполяции на каждый пиксель вектор нормали v_normal перестает быть единичным, поэтому обязательно нужно его повторно нормализовать в промежуточный вектор n_normal, который далее можно использовать при расчете освещения.
Расчет освещения в шейдерах будет рассмотрен в дальнейших уроках.
Конечная цель фрагментного шейдера - это получение цвета пикселя. Рассчитанный цвет пикселя должен быть обязательно записан в системную переменную gl_FragColor. В нашем простейшем примере мы не вычисляем цвет пикселя в фрагментном шейдере, а просто присваиваем значение цвета v_color, полученного путем интерполяции из цветов вершин:
gl_FragColor = v_color;

Получение матриц.
Чтобы передать матрицу модели-вида-проекции u_modelViewProjectionMatrix в вершинный шейдер нужно ее получить.
Матрица модели.
Матрица модели описывает собственное движение вершин, из которых состоит модель, в трехмерном пространстве. Простейший случай модели - это одна вершина. Рассмотрим для примера поворот. Например, нам нужно повернуть вершину с координатами x,y,z вершину на угол angle против часовой стрелки относительно вектора с координатами rotateVectorX, rotateVectorY, rotateVectorZ, проходящего через начало координат. Для определения матрицы поворота существует команда Matrix.setRotateM. Получим матрицу поворота modelMatrix.
Определим пустой массив для матрицы модели:
float[] modelMatrix = new float[16];
Применим команду: 
Matrix.setRotateM(modelMatrix, 0, angle, rotateVectorX, rotateVectorY, rotateVectorZ);
Получим заполненный массив modelMatrix, соответствующий нашему повороту.
Как получить новые координаты вершины после поворота ? Достаточно умножить матрицу модели на вектор координат вершин:
//запишем текущие координаты вершин в четырехкомпонентный вектор
float vertex[]={x,y,z,1};
//создадим пустой массив, в который будут записаны новые координаты
float new_vertex=new float[4];
//умножим матрицу модели на вектор координат
Matrix.multiplyMV(new_vertex, 0, modelMatrix, 0, vertex, 0);
//получили новые координаты после поворота
float new_x=new_vertex[0];
float new_y=new_vertex[1];
float new_z=new_vertex[2];
Команда Matrix.multiplyMV умножает матрицу модели modelMatrix на вектор координат вершины vertex и записывает результат в массив new_vertex. Если модель состоит из множества  жестко связанных вершин, совершающих одинаковую трансформацию, нужно умножить матрицу модели на координаты каждой вершины. Если вершин несколько тысяч - это может быть затратно по времени, если трансформации вершин производить на CPU. Поэтому, в этом случае удобнее передать матрицу модели в вершинный шейдер как униформу и преобразования координат вершин выполнять в вершинном шейдере. Если вершин немного, можно оставить расчет модельных трансформаций вне шейдера.

Матрица вида.
Матрица вида однозначно связана с координатами камеры. Зная положение и ориентацию камеры мы можем всегда получить матрицу вида. Для этого в классе android.opengl.Matrix существует специальная функция Matrix.setLookAtM
Определим пустой массив для матрицы вида:
float[] viewMatrix = new float[16];
Зададим положение камеры пространстве для примера:
float xposition=0.3f;
float yposition=1.7f;
float zposition=1.5f;
Зададим точку в пространстве, на которую смотрит камера. Пусть камера смотрит на начало мировых координат:
float xlook=0;
float ylook=0;
float zlook=0;
Для однозначного определения матрицы вида этих данных мало, т.к. недостаточно установить наблюдателя в точку и правильно направить его взгляд. Нужно еще запретить наблюдателю качать головой. Зададим вектор, который определит, где у камеры верх. Пусть верх будет вдоль оси Y:
float xtop=0;
float ytop=1;
float ztop=0;
А затем вызовем функцию функция Matrix.setLookAtM, которая рассчитает и заполнит  массив viewMatrix:   
Matrix.setLookAtM(viewMatrix, 0, xposition, yposition, zposition, xlook, ylook, zlook, xtop, ytop, ztop);
Матрица вида готова.

Матрица модели-вида.
Часто комбинируют матрицы модели и вида в единую матрицу модели-вида, которую можно получить путем умножения матрицы вида на матрицу модели. Для умножения двух матриц используют команду Matrix.multiplyMM.
float[] modelViewMatrix = new float[16];
Matrix.multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0);
Получили заполненный массив modelViewMatrix.

Матрица проекции.
Матрица проекции выполняет проекцию координат вершин на экран аппарата после модельно-видовых трансформаций. Она может получена в методе onSurfaceChanged  класса рендерера путем выполнения команды Matrix.frustumM для перспективной проекции. Например, так:
............
float projectionMatrix=new float[16];
............
public void onSurfaceChanged(GL10 unused, int width, int height) {
       // устанавливаем glViewport
       GLES20.glViewport(0, 0, width, height);
       float ratio = (float) width / height;
       float k=0.055f;
       float left = -k*ratio;
       float right = k*ratio;
       float bottom = -k;
       float top = k;
       float near = 0.1f;
       float far = 10.0f;
       // получаем матрицу проекции
      Matrix.frustumM(projectionMatrix, 0, left, right, bottom, top, near, far);
}
Матрица модели-вида-проекции.
Умножив матрицу проекции на матрицу модели-вида получаем комбинированную матрицу модели-вида-проекции:
float modelViewProjectionMatrix=new float[16];
Matrix.multiplyMM(modelViewProjectionMatrix,0,projectionMatrix, 0, modelViewMatrix, 0);
Матрица модели-вида-проекции получена. Напомню, что  матрица модели-вида-проекции передается в вершинный шейдер как униформа. Вопрос по передаче данных униформ в шейдеры будет рассмотрен позже.

Создание шейдерного объекта в OpenGL ES 2.0
Шейдерный объект состоит из кодов вершинного и фрагментного шейдеров и так называемой "программы". Для использования шейдерного объекта нужно получить ссылку на данную программу. Опишем последовательность действий при создании шейдерного объекта.

1. Запишем коды вершинного и фрагментного шейдеров в строковые переменные:
String vertexShaderCode=
"uniform mat4 u_modelViewProjectionMatrix;"+
"attribute vec3 a_vertex;"+
"attribute vec3 a_normal;"+
"attribute vec4 a_color;"+
"varying vec3 v_vertex;"+
"varying vec3 v_normal;"+
"varying vec4 v_color;"+
"void main() {"+
"         v_vertex=a_vertex;"+
"         vec3 n_normal=normalize(a_normal);"+
"         v_normal=n_normal;"+
"         v_color=a_color;"+
"        gl_Position = u_modelViewProjectionMatrix * vec4(a_vertex,1.0);"+
"}"; 
String fragmentShaderCode=
"precision mediump float;"+
"varying vec3 v_vertex;"+
"varying vec3 v_normal;"+
"varying vec4 v_color;"+
"void main() {"+
"        vec3 n_normal=normalize(v_normal);"+
"        gl_FragColor = v_color;"+
"}";

2. Получим свободный номер вершинного шейдера:
int vertexShader_Handle = GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
В дальнейшем к вершинному шейдеру можно обращаться по этому номеру. Целое число vertexShader_Handle является ссылкой на область памяти, выделенную для хранения вершинного шейдера.

3. Передаем в вершинный шейдер его код:
GLES20.glShaderSource(vertexShader_Handle, vertexShaderCode);
Здесь vertexShader_Handle - это полученная в предыдущем пункте ссылка на вершинный шейдер, а vertexShaderCode-код вершинного шейдера в виде строки.

4. Компилируем вершинный шейдер:
GLES20.glCompileShader(vertexShader_Handle);
При этом код вершинного шейдера переводится в инструкции, понятные видеокарте.

5.Аналогичные операции проводим с фрагментным шейдером:
// получаем ссылку на фрагментный шейдер
int fragmentShader_Handle = GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
// загружаем в фрагментный шейдер его код
GLES20.glShaderSource(fragmentShader_Handle, fragmentShaderCode);
// компилируем фрагментный шейдер
GLES20.glCompileShader(fragmentShader_Handle);

6.Получим свободный номер "программы":
int program_Handle = GLES20.glCreateProgram();
В дальнейшем к программе можно обращаться по этому номеру. Целое число program_Handle является ссылкой на программу.

7. Присоединим к программе скомпилированный вершинный шейдер:
GLES20.glAttachShader(program_Handle, vertexShader_Handle); 
Первый аргумент program_Handle - ссылка на программу, второй vertexShader_Handle - ссылка на вершинный шейдер.

8.Аналогично присоединим к программе скомпилированный фрагментный шейдер:
GLES20.glAttachShader(program_Handle, fragmentShader_Handle); 

9.Компилируем программу:
GLES20.glLinkProgram(program_Handle);    

Шейдерный объект готов к работе. Мы можем создать несколько шейдерных объектов и переключаться между ними командой:
GLES20.glUseProgram(program_Handle);
Данная команда указывает, что в данный момент активным является объект со ссылочным номером программы program_Handle и соответственно будут работать его вершинный и фрагментный шейдеры. Поэтому нужно сохранять program_Handle как поле класса.    

Передача данных униформ в шейдеры.
Напомню, что нам нужно передать униформу матрицы модели-вида-проекции в вершинный шейдер. Какие действия для этого нужно выполнить ?
Сначала необходимо выбрать активную шейдерную программу:
GLES20.glUseProgram(program_Handle);
Затем получить внешнюю ссылку на униформу:  
int u_modelViewProjectionMatrix_Handle = 
       GLES20.glGetUniformLocation(program_Handle, "u_modelViewProjectionMatrix");
Первый аргумент-это текущий ссылочный номер программы. Внутри вершинного шейдера матрица модели-вида-проекции называется как u_modelViewProjectionMatrix. Имя униформы передается в команду в качестве второго аргумента в виде строки. В результате выполнения команды получим ссылочный номер u_modelViewProjectionMatrix_Handle, который позволяет нам обращаться к униформе  u_modelViewProjectionMatrix извне.
Связываем массив modelViewProjectionMatrix и униформу:  
GLES20.glUniformMatrix4fv(
        u_modelViewProjectionMatrix_Handle, 1, false, modelViewProjectionMatrix, 0);
Первый аргумент - это ссылка на униформу, которую мы получили через команду glGetUniformLocation. Второй аргумент 1 - это размерность элементов матрицы. Элементы матрицы модели-вида-проекции это одиночные числа с плавающей точкой (в общем случае элементами матрицы могут быть векторы, тогда размерность может быть 2,3, ну это уже лишнее). Третий аргумент false - признак транспонирования. Показывает нужно ли транспонировать матрицу перед передачей в шейдер. Это нам не понадобится. Четвертый аргумент modelViewProjectionMatrix - это источник данных, т.е. наш массив размером 16 элементов, в котором записана матрица модели-вида- проекции. Пятый аргумент 0 - это сдвиг. Никаких сдвигов мы использовать не будем. Все прочие матрицы размера 4х4 (например модели-вида) передаются в шейдер аналогично при помощи команды glUniformMatrix4fv, если в них есть потребность. 
Рассмотри другие случаи. Например, нам нужно передать в шейдер координаты источника света xLightPosition, yLightPosition, zLightPosition. Можно сформировать из координат трехэлементный массив и предать его в шейдер:
float [] lightPosition = {xLightPosition, yLightPosition, zLightPosition};
// получаем ссылку на униформу u_lightPosition
int u_lightPosition_Handle=GLES20.glGetUniformLocation(program_Handle, "u_lightPosition");
// связываем наш массив lightPosition с униформой u_lightPosition
GLES20.glUniform3fv(u_lightPosition_Handle, 1, lightPosition, 0);
Само название функции glUniform3fv говорит о том, что мы будем передавать в шейдер векторы из трех компонент. Первый аргумент u_lightPosition_Handle является ссылкой на униформу u_lightPosition. Второй аргумент 1- это количество передаваемых элементов. В нашем случае мы передаем один вектор, поэтому ставим единицу. Можно передавать массив трехкомпонентных векторов (т.е. массив массивов или двумерный массив), но мы этим заниматься не будем. Третий аргумент  lightPosition - наш массив, в котором содержатся координаты источника света.
Существует другой способ передачи координат источника света без массива напрямую по компонетам. Например так:
u_lightPosition_Handle=GLES20.glGetUniformLocation(program_Handle, "u_lightPosition");
GLES20.glUniform3f(u_lightPosition_Handle, xLightPosition, yLightPosition, zLightPosition);
Здесь вместо векторной формы команды glUniform3fv используется скалярная glUniform3f. Какую форму использовать - дело вкуса.
Аналогично можно передать в шейдер координаты камеры:
int u_camera_Handle=GLES20.glGetUniformLocation(program_Handle, "u_camera");
GLES20.glUniform3f(u_camera_Handle, xСamera, yСamera, zСamera);
Наконец, рассмотрим самый простой случай - передачи числа с плавающей точкой.  Допустим нужно связать число arg с униформой u_arg:
int u_arg_Handle=GLES20.glGetUniformLocation(program_Handle, "u_arg");
GLES20.glUniform1f(u_arg_Handle, arg);

Передача атрибутов вершин в вершинный шейдер.
В отличие от униформ атрибуты являются свойствами вершины и должны быть определены для каждой вершины рисуемого объекта. Кроме того, связь массивов данных с атрибутами в вершинном шейдере устанавливается не напрямую, а через буферы типа FloatBuffer. Для примера рассмотрим передачу в шейдер атрибутов банального треугольника. Пусть координаты точки A треугольника будут xa, ya, za, координаты точки B - xb, yb, yb, точки C - xc, yc, zc. Создадим массив, последовательно перечисляющий координаты треугольника в порядке обхода вершин A-->B-->C:
float vertexArray []={xa, ya, za,  xb, yb, yb,  xc, yc, zc};
Затем перепишем его в буфер vertexBuffer:
ByteBuffer b1 = ByteBuffer.allocateDirect(36);
b1.order(ByteOrder.nativeOrder());
FloatBuffer vertexBuffer = b1.asFloatBuffer();
vertexBuffer.position(0);
vertexBuffer.put(vertexArray);
vertexBuffer.position(0);
Координаты вершин записаны в буфер. Теперь нужно связать этот буфер с соответствующим атрибутом  a_vertex внутри шейдера.
// выбираем текущую программу 
GLES20.glUseProgram(program_Handle);
// получаем ссылку на атрибут a_vertex в вершинном шейдере
int a_vertex_Handle = GLES20.glGetAttribLocation(program_Handle, "a_vertex");
// включаем использование данного атрибута  
GLES20.glEnableVertexAttribArray(a_vertex_Handle);
//связываем наш буфер vertexBuffer с атрибутом a_vertex внутри шейдера
GLES20.glVertexAttribPointer(a_vertex_Handle, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer);
Рассмотрим команду glVertexAttribPointer подробнее. Первый аргумент a_vertex_Handle - это ссылка на атрибут координаты вершины. Второй аргумент 3 - это размерность атрибута, т.е. количество компонент на вершину. Каждая вершина имеет три координаты в пространстве - поэтому размерность будет равна трем. Третий аргумент  GLES20.GL_FLOAT указывает что атрибут состоит из чисел с плавающей точкой. Четвертый аргумент false - признак нормализации. Он указывает нужно ли нормализовать вектор из трех координат перед передачей в шейдер. Четвертый аргумент 0-сдвиг. Пятый аргумент - это наш буфер координат вершин vertexBuffer. Итак, мы установили связь между буфером координат вершин и атрибутом координат вершин в шейдере. Это связь будет сохранена даже если вы будете менять координаты вершин в процессе рендинга. Т.е если вы перезаписываете буфер vertexBuffer в цикле не нужно каждый раз выполнять команду glVertexAttribPointer. Измененные данные атрибута автоматически попадут в вершинный шейдер. Однако, если шейдерная программа будет разрушена или уничтожен объект vertexBuffer связь придется устанавливать заново.
Аналогично установим связь для другого атрибута вершины - вектора нормали. У одиночного треугольника нормаль одинакова для всех трех вершин и рассчитывается как векторное произведение двух векторов: A-->B и A-->C:
float x1=xb-xa;
float y1=yb-ya;
float z1=zb-za;
float x2=xc-xa;
float y2=yc-ya;
float z2=zc-za;
float xn=y1*z2-y2*z1;
float yn=x2*z1-x1*z2;
float zn=x1*y2-x2*y1;
Получаем координаты вектора нормали xn,yn,zn. Cоздадим буфер для хранения координат нормали и установим его связь с атрибутом a_normal:
float normalArray []={xn, yn, zn, xn, yn, zn, xn, yn, zn};
ByteBuffer b2 = ByteBuffer.allocateDirect(36);
b2.order(ByteOrder.nativeOrder());
FloatBuffer normalBuffer = b2.asFloatBuffer();
normalBuffer.position(0);
normalBuffer.put(normalArray);
normalBuffer.position(0);
GLES20.glUseProgram(program_Handle);
int a_normal_Handle = GLES20.glGetAttribLocation(program_Handle, "a_normal"); 
GLES20.glEnableVertexAttribArray(a_normal_Handle);
GLES20.glVertexAttribPointer(a_normal_Handle, 3, GLES20.GL_FLOAT, false, 0, normalBuffer);
Аналогично можно связать цвета вершин с атрибутом a_color. Разница только в том, что у цвета четыре компонента красный, зеленый, синий и альфа.
float colorArray []={
redA, greenA, blueA, alphaA,
redB, greenB, blueB, alphaB,
redC, greenC, BlueC, alphaC};
ByteBuffer b3 = ByteBuffer.allocateDirect(48);
b3.order(ByteOrder.nativeOrder());
FloatBuffer colorBuffer = b3.asFloatBuffer();
colorBuffer.position(0);
colorBuffer.put(colorArray);
colorBuffer.position(0);
GLES20.glUseProgram(program_Handle);
int a_color_Handle = GLES20.glGetAttribLocation(program_Handle, "a_color"); 
GLES20.glEnableVertexAttribArray(a_color_Handle);
GLES20.glVertexAttribPointer(a_color_Handle, 4, GLES20.GL_FLOAT, false, 0, colorBuffer);


Класс шейдерного объекта.
С точки зрения объектно-ориентированного программирования удобно создать отдельный класс шейдерного объекта, внутри него хранить ссылку на шейдерную программу и методы связывающие униформы и атрибуты с внешними данными. Создадим такой класс.
public class Shader {
//будем хранить ссылку на шейдерную программу
//внутри класса как локальное поле
private int program_Handle;

//при создании объекта класса передаем в конструктор
//строки кода вершинного и фрагментного шейдера

public Shader(String vertexShaderCode, String fragmentShaderCode){
      //вызываем метод, создающий шейдерную программу
      //при этом заполняется поле program_Handle

      createProgram(vertexShaderCode, fragmentShaderCode);
}
// метод, который создает шейдерную программу, вызывается в конструкторе
private void createProgram(String vertexShaderCode, String fragmentShaderCode){
      //получаем ссылку на вершинный шейдер  
      int vertexShader_Handle = 
              GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER);
      //присоединяем к вершинному шейдеру его код
      GLES20.glShaderSource(vertexShader_Handle, vertexShaderCode);
      //компилируем вершинный шейдер
      GLES20.glCompileShader(vertexShader_Handle);
      //получаем ссылку на фрагментный шейдер
      int fragmentShader_Handle = 
               GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER);
      //присоединяем к фрагментному шейдеру его код
      GLES20.glShaderSource(fragmentShader_Handle, fragmentShaderCode);
      //компилируем фрагментный шейдер
      GLES20.glCompileShader(fragmentShader_Handle);
      //получаем ссылку на шейдерную программу
      program_Handle = GLES20.glCreateProgram();
      //присоединяем к шейдерной программе вершинный шейдер
      GLES20.glAttachShader(program_Handle, vertexShader_Handle);
      //присоединяем к шейдерной программе фрагментный шейдер
      GLES20.glAttachShader(program_Handle, fragmentShader_Handle);
      //компилируем шейдерную программу
      GLES20.glLinkProgram(program_Handle);
}

//метод, который связывает 
//буфер координат вершин vertexBuffer с атрибутом a_vertex
public void linkVertexBuffer(FloatBuffer vertexBuffer){
      //устанавливаем активную программу
      GLES20.glUseProgram(program_Handle);
      //получаем ссылку на атрибут a_vertex
      int a_vertex_Handle = GLES20.glGetAttribLocation(program_Handle, "a_vertex");
      //включаем использование атрибута a_vertex
      GLES20.glEnableVertexAttribArray(a_vertex_Handle);
      //связываем буфер координат вершин vertexBuffer с атрибутом a_vertex
      GLES20.glVertexAttribPointer(
              a_vertex_Handle, 3, GLES20.GL_FLOAT, false, 0,vertexBuffer);
}


//метод, который связывает 
//буфер координат векторов нормалей normalBuffer с атрибутом a_normal
public void linkNormalBuffer(FloatBuffer normalBuffer){
      //устанавливаем активную программу
      GLES20.glUseProgram(program_Handle);
     //получаем ссылку на атрибут a_normal
     int a_normal_Handle = GLES20.glGetAttribLocation(program_Handle, "a_normal");
     //включаем использование атрибута a_normal
     GLES20.glEnableVertexAttribArray(a_normal_Handle);
    //связываем буфер нормалей normalBuffer с атрибутом a_normal
    GLES20.glVertexAttribPointer(
              a_normal_Handle, 3, GLES20.GL_FLOAT, false, 0,normalBuffer);
}

//метод, который связывает 
//буфер цветов вершин colorBuffer с атрибутом a_color
public void linkColorBuffer(FloatBuffer colorBuffer){
      //устанавливаем активную программу
      GLES20.glUseProgram(program_Handle); 
     //получаем ссылку на атрибут a_color
     int a_color_Handle = GLES20.glGetAttribLocation(program_Handle, "a_color"); 
     //включаем использование атрибута a_color
     GLES20.glEnableVertexAttribArray(a_color_Handle);
    //связываем буфер нормалей colorBuffer с атрибутом a_color
    GLES20.glVertexAttribPointer(
              a_color_Handle, 4, GLES20.GL_FLOAT, false, 0, colorBuffer); 
}

//метод, который связывает матрицу модели-вида-проекции
// modelViewProjectionMatrix с униформой u_modelViewProjectionMatrix
public void linkModelViewProjectionMatrix(float [] modelViewProjectionMatrix){
      //устанавливаем активную программу
      GLES20.glUseProgram(program_Handle);
      //получаем ссылку на униформу u_modelViewProjectionMatrix
      int u_modelViewProjectionMatrix_Handle = 
          GLES20.glGetUniformLocation(program_Handle, "u_modelViewProjectionMatrix");
     //связываем массив modelViewProjectionMatrix
     //с униформой u_modelViewProjectionMatrix
     GLES20.glUniformMatrix4fv(
          u_modelViewProjectionMatrix_Handle, 1, false, modelViewProjectionMatrix, 0);
}


// метод, который связывает координаты камеры с униформой u_camera
public void linkCamera (float xCamera, float yCamera, float zCamera){
      //устанавливаем активную программу
      GLES20.glUseProgram(program_Handle);
      //получаем ссылку на униформу u_camera
      int u_camera_Handle=GLES20.glGetUniformLocation(program_Handle, "u_camera");
      // связываем координаты камеры с униформой u_camera
      GLES20.glUniform3f(u_camera_Handle, xCamera, yCamera, zCamera);
}

// метод, который связывает координаты источника света 
// с униформой u_lightPosition
public void linkLightSource (float xLightPosition, float yLightPosition, float zLightPosition){
      //устанавливаем активную программу
      GLES20.glUseProgram(program_Handle);
      //получаем ссылку на униформу u_lightPosition
      int u_lightPosition_Handle=GLES20.glGetUniformLocation(program_Handle, "u_lightPosition");
      // связываем координаты источника света с униформой u_lightPosition
      GLES20.glUniform3f(u_lightPosition_Handle, xLightPosition, yLightPosition, zLightPosition);
}

// метод, который делает шейдерную программу данного класса активной
public void useProgram(){
      GLES20.glUseProgram(program_Handle);
}
// конец класса
}

На этом наш первый урок закончен. В следующих уроках мы применим класс Shader на практике. Ждите следующих уроков.

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

  1. Большое спасибо. Молодец. Книги пиши)

    ОтветитьУдалить
  2. Главное что всё работательное! )
    Есть вот такая схема: 2 модели и для каждой модели по вершинному шейдеру, т.е. 2 шейдера А и Б.
    Добавляется новая фича изменения геометрии 3м вершинным шейдером В.
    Пришлось сделать 4 исходных шейдера (В А) (В Б) (А) (Б), может я сделал глупо, но я так и не понял как сделать конвейер из вершинных шейдеров и можно вообще так делать? И как взаимодействовать между ними если это возможно?
    Хотелось вот что-то типа:
    GLES20.glAttachShader(m_program, addonVertexShader);
    GLES20.glAttachShader(m_program, mainVertexShader);
    GLES20.glAttachShader(m_program, fragmentShader);
    GLES20.glLinkProgram(m_program);
    Подскажите пожайлуста если известно как сделать для mainVertexShader что-то типа фильтра геометрии и как передать что-либо из addonVertexShader в mainVertexShader

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

    ОтветитьУдалить
  4. атрибуты и юниформы неизменяемые для вершинного шейдера, а varying переменная не перенесет значение между вершинными шейдерами до того как попадёт во фрагментный?

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

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

      Удалить
    3. Можно написать один универсальный шейдер. В шейдерах можно объявлять пользовательские функции, которые могут выполнять различные действия в зависимости от параметров. Например:
      vec3 myfunction(vec3 vectorIn1, vec3 vectorIn2, int mode){
      vec3 vectorOut;
      if (mode==0){
      vectorOut=....
      }else if(mode==1){
      vectorOut=....
      }else{
      vectorOut=....
      }
      return vectorOut;
      }
      и вызывать их из функции main

      Удалить
  5. Вопрос: Как передать в шейдер массив определенной длины? Например у меня в программе может быть от 1 до 10 источников света. К примеру по результатам рендинга за 2-3 секунды их кол-во может быть изменено. хотелось бы каким-то образом обработать их передачу.... просто использования uniform vec3 u_LightPosition_1;
    uniform vec3 u_LightPosition_2;
    и т.д не очень удобное... в идеале бы описать вспомогательный шейдер (второй) в котором буду проводить все математические вычисления над этим масивом...

    ОтветитьУдалить
    Ответы
    1. Я сейчас как раз занимаюсь множественным освещением. Дам ответ чуть позже.

      Удалить
    2. Ну и хорошо что опредленной длины )
      в шейдере массив определяется синтаксисом
      uniform mat4 AttribGib[10];

      В программе обращение к элементу этого массива вот так
      int location = GLES20.glGetUniformLocation(
      programid(m_program),
      "AttribGib[" + number + "]");
      т.е. имя формируется синтаксическим обращением к массиву
      GLES20.glUniformMatrix4fv(
      location,
      count(1),
      transpose(false),
      matrix(body.gibs[number]),
      offset(0))
      про удобство я тут тоже писал, типа фильтра перед молотящим шейдером, аннет не получится, main всего 1

      Удалить
    3. Пример для массива из двух источников света:
      1.Создадим массив для хранения координат двух источников света
      lightPositionArray={xLightPosition0, yLightPosition0, zLightPosition0,xLightPosition1, yLightPosition1, zLightPosition1};

      2.Дополним класс Shader методом, связывающим массив с униформой u_lightPosition_array:
      public void linkLightSource (float [] lightPositionArray){
      GLES20.glUseProgram(program_Handle);
      int u_lightPosition_array_Handle=GLES20.glGetUniformLocation(program_Handle, "u_lightPosition_array"); GLES20.glUniform3fv(u_lightPosition_array_Handle, 3, lightPositionArray,0);
      }

      3. В методе onDrawFrame передадим массив источников света в шейдер
      mShader.linkLightSource(lightPositionArray);

      4. Изменим код фрагментного шейдера
      precision mediump float;
      uniform vec3 u_camera;+
      uniform vec3 u_lightPosition_array[2];
      varying vec3 v_vertex;
      varying vec3 v_normal;
      vec4 one=vec4(1.0,1.0,1.0,1.0);
      float ambient=0.1;
      float k_diffuse=0.7;
      float k_specular=0.3;

      //функция которая возвращает цвет для одного источника света
      vec4 getLight(vec3 lightPosition, vec3 vertex, vec3 normal){
      vec3 lightvector = normalize(lightPosition - vertex);
      vec3 lookvector = normalize(u_camera - vertex);
      float diffuse = k_diffuse * max(dot(normal, lightvector), 0.0);
      vec3 reflectvector = reflect(-lightvector, normal);
      float specular = k_specular * pow( max(dot(lookvector,reflectvector),0.0), 40.0 );
      vec4 light=(ambient+diffuse+specular)*one;
      return light;
      }

      void main() {
      vec3 n_normal=normalize(v_normal);
      vec4 lightColor=vec4(0.0, 0.0, 0.0, 1.0);
      //суммируем цвета от всех источников света
      for (int i=0; i<2; i++){
      lightColor+=getLight(u_lightPosition_array[i], v_vertex, n_normal);"+
      }
      //делим полученный цвет на количество источников
      lightColor=lightColor/2.0;
      gl_FragColor=lightColor;
      }
      У меня этот код работает.

      Удалить
  6. Андрей подскажите плиз начинающему стоит ли читать ваши уроки по Open GL ES 1.x
    если я хочу писать программы под Android используя только Open GL ES 2.x или надо сразу начинать читать про Шейдеры?

    И на насчет литературы есть много книг по Open GL ES 1.x есть смысл их читать или они уже устарели для современных девайсов (например Galaxy SIII)?

    ОтветитьУдалить
    Ответы
    1. Если ogles 2.0, то сразу шейдеры, сомневаюсь что в 2.0 наберется хотя бы 50% того, чем рисовали в предыдущих версиях

      Удалить
    2. OpenGL ES 1.0 безнадежно устарел. Поэтому начинайте сразу с шейдеров.

      Удалить
  7. Спасибо Андрей просто по OpenGL ES 1.0 очень много литературы думал сначала с него начать а то шейдеры пока сложно понять.

    ОтветитьУдалить
  8. Андрей статья по шейдерам очень хорошая но не хватает картинок тяжело воспринимать. Может есть какой-нибудь софт для разработки шейдеров где наглядно показыватся как оно будет выглядеть?

    ОтветитьУдалить
    Ответы
    1. GLSL - простой язык. Если хоть немного знакомы с C или C++ освоить не составит труда. Более подробную информацию о GLSL можно получить в книге Алексея Борескова "Разработка и отладка шейдеров". Софт для разработки шейдеров существует и называется он RenderMonkey, но он к Android не приспособлен. Поэтому я им не пользуюсь.

      Удалить
    2. А может у вас есть сборник литературы который вы используете? Я был бы рад видеть обзор подобных ссылок и документов :)

      Удалить
    3. Много полезной информации на сайте http://www.learnopengles.com, там внизу есть раздел Android Tutorials. Про шейдеры можно почитать книгу Алексея Борескова "Разработка и отладка шейдеров"

      Удалить
  9. Андрей у меня как раз нет проблем с Assembler X86/C/C++/C#/Java просто я пишу пока и не представляю как оно будет выглядеть тяжело представить как оно будет смотреться в пространстве привык что в отладчике можно видеть пошагово весь процесс работы твоей программы. За книгу большое спасибо. И кстати насчет RenderMonkey там же есть поддержка OpenGL и OpenGL ES или в Android какой-то другой OpenGL ES используется?
    p.s. просто я в программировании графики новичок.

    ОтветитьУдалить
  10. Большое спасибо! Очень нужное описание работы с шейдерами даже с точки зрения их программирования под WEBGL.

    ОтветитьУдалить
  11. Спасибо за труды!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

    ОтветитьУдалить
  12. Спасибо Вам за такие уроки. У меня вопрос, который на мой взгляд скорее всего касается именно текстур.
    На сколько я понимаю, существует проблема, когда вы с Хоум скрина переключаетесь на какое-то приложение и возвращаетесь обратно, например на хоум скрин, в случае Живых обоев, и по сути происходит повторная загрузка текстур, что приводит в некоторых случаях к довольно продолжительной 3-5 сек паузе.
    Не сталкивались ли вы с методами решения данной проблемы? Есть ли какие-то варианты не производить каждый раз загрузку текстур повторно?

    ОтветитьУдалить
    Ответы
    1. Грузятся долго не текстуры, а долго создаются объекты Bitmap из графических файлов командой BitmapFactory.decodeResource(context.getResources(), idpicture).

      Создание текстурного объекта из bitmap через GLUtils.texImage2D(GLES20.GL_TEXTURE_2D,0,bitmap,0) выполняется быстро - 20-30 мсек.

      Поэтому нужно создать один раз объект класса Bitmap из картинки, удерживать bitmap в памяти, а уже из готового bitmap выполнять загрузку текстур.
      Можно объявить bitmap статическим полем в классе MyClassRenderer, например так:
      public static Bitmap bitmap;

      Далее в конструкторе класса MyClassRenderer создаем объект bitmap из картинки:
      bitmap = bitmapFactory.decodeResource(context.getResources(),R.drawable.picture);
      При этом статический объект bitmap создается в памяти один раз и удерживается в ней до выхода из программы.
      А в методе onSurfaceCreated мы будем просто копировать bitmap в память видеокарты, т.е. создавать текстурный объект.
      Для этого в классе Texture создаем дополнительный конструктор, который грузит текстуру из объекта bitmap:
      // конструктор двумерной текстуры из bitmap
      public Texture(Bitmap bitmap){
      name=0;
      names = new int[1];
      GLES20.glGenTextures(1, names, 0);
      name = names[0];
      GLES20.glPixelStorei(GLES20.GL_UNPACK_ALIGNMENT, 1);
      GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, name);
      // устанавливаем параметры текстуры
      GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR_MIPMAP_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);
      //переписываем bitmap в текстуру
      GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
      //создаем мипмапы
      GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
      }

      В методе onSurfaceCreated создаем текстуру из bitmap
      mTexture=new Texture(bitmap);

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

      Удалить
    2. Спасибо. Я попробую.

      Удалить
  13. Из собственного опыта о полезности Шейдеров.
    Недавно их сипсользовл для сокращения размера приложения. В приложении несколько фонов, разных цветов с определенной текстурой. За место хранения всех текстур, храню одну в черно-белых тонах, которая задает фактуру, и потом окрашваю шейдерами.

    ОтветитьУдалить
  14. Замечательная статья, спасибо огромное! Перечитал кучу статей - ничего не понял. Прочитал эту - все стало ясно!

    ОтветитьУдалить
  15. Этот комментарий был удален автором.

    ОтветитьУдалить
    Ответы
    1. Спасибо за статью, интересно и понятно.

      Удалить
  16. Этот комментарий был удален автором.

    ОтветитьУдалить
  17. Как загрузить карту высот через Bitmap. Прочёл книгу забугорную и не понял ничего. Помогите пожалуйста. Книгу скачал ту, которая была указана на сайте http://www.learnopengles.com

    ОтветитьУдалить