OpenGL ES 2.0. Урок первый-Шейдеры
Введение в шейдеры. OpenGL ES 2.0 использует шейдеры языка GLSL. Шейдеры бывают двух типов - вершинный и фрагментный. В вершинном шейдере производятся расчеты над вершинами, а в фрагментном - над пикселями. Рассмотрим простой вершинный шейдер: uniform mat4 u_modelViewProjectionMatrix; attribute vec3 a_vertex; attribute vec3 a_normal; varying vec3 v_vertex; varying vec3 v_normal; varying vec4 v_color; void main() { v_vertex=a_vertex; 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; v_normal=n_normal; v_color=a_color; ....... В вершинный шейдер мы передаем атрибут вектора нормали a_normal, который не обязательно должен быть нормализованным, поэтому произведем его нормализацию в промежуточный вектор n_normal, который затем отправим на интерполяцию: v_normal=n_normal; В фрагментном шейдере получим значение координат вектора нормали v_normal для каждого пикселя. v_color=a_color; Копируем атрибут цвета вершины в varying. В фрагментном шейдере получим значение цвета v_color для каждого пикселя. Следует отметить, что данные в varying - переменные можно записать только в вершинном шейдере, для фрагментного шейдера они доступны только для чтения.
Завершает наш вершинный шейдер строка: Системная переменная 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 new_vertex=new float[4]; Команда 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); Матрица вида готова.
Матрица модели-вида. Матрица проекции. Матрица проекции выполняет проекцию координат вершин на экран аппарата после модельно-видовых трансформаций. Она может получена в методе onSurfaceChanged класса рендерера путем выполнения команды Matrix.frustumM для перспективной проекции. Например, так: ............ Матрица модели-вида-проекции получена. Напомню, что матрица модели-вида-проекции передается в вершинный шейдер как униформа. Вопрос по передаче данных униформ в шейдеры будет рассмотрен позже.
Создание шейдерного объекта в OpenGL ES 2.0 Шейдерный объект состоит из кодов вершинного и фрагментного шейдеров и так называемой "программы". Для использования шейдерного объекта нужно получить ссылку на данную программу. Опишем последовательность действий при создании шейдерного объекта.
"uniform mat4 u_modelViewProjectionMatrix;"+ "attribute vec3 a_vertex;"+ "attribute vec3 a_normal;"+ "varying vec3 v_vertex;"+ "varying vec3 v_normal;"+ "varying vec4 v_color;"+ "void main() {"+ " v_vertex=a_vertex;"+ " 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 является ссылкой на область памяти, выделенную для хранения вершинного шейдера.
Здесь vertexShader_Handle - это полученная в предыдущем пункте ссылка на вершинный шейдер, а vertexShaderCode-код вершинного шейдера в виде строки.
При этом код вершинного шейдера переводится в инструкции, понятные видеокарте.
6. Получим свободный номер "программы": В дальнейшем к программе можно обращаться по этому номеру. Целое число program_Handle является ссылкой на программу.
Первый аргумент program_Handle - ссылка на программу, второй vertexShader_Handle - ссылка на вершинный шейдер.
9.Компилируем программу: Шейдерный объект готов к работе. Мы можем создать несколько шейдерных объектов и переключаться между ними командой: 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"); Здесь вместо векторной формы команды glUniform3fv используется скалярная glUniform3f. Какую форму использовать - дело вкуса. Аналогично можно передать в шейдер координаты камеры: int u_camera_Handle=GLES20.glGetUniformLocation(program_Handle, "u_camera"); Наконец, рассмотрим самый простой случай - передачи числа с плавающей точкой. Допустим нужно связать число 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); vertexBuffer.position(0); Координаты вершин записаны в буфер. Теперь нужно связать этот буфер с соответствующим атрибутом a_vertex внутри шейдера. // выбираем текущую программу GLES20.glUseProgram(program_Handle); Рассмотрим команду 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); Аналогично можно связать цвета вершин с атрибутом a_color. Разница только в том, что у цвета четыре компонента красный, зеленый, синий и альфа. float colorArray []={
Класс шейдерного объекта. С точки зрения объектно-ориентированного программирования удобно создать отдельный класс шейдерного объекта, внутри него хранить ссылку на шейдерную программу и методы связывающие униформы и атрибуты с внешними данными. Создадим такой класс. public class Shader {
/ / метод, который создает шейдерную программу, вызывается в конструкторе private void createProgram (String vertexShaderCode, String fragmentShaderCode){ //получаем ссылку на вершинный шейдер GLES20.glCreateShader(GLES20.GL_VERTEX_SHADER); GLES20.glCreateShader(GLES20.GL_FRAGMENT_SHADER); //метод, который связывает буфер координат вершин vertexBuffer с атрибутом a_vertex public void linkVertexBuffer (FloatBuffer vertexBuffer){ //устанавливаем активную программу //получаем ссылку на атрибут a_vertex //включаем использование атрибута a_vertex //связываем буфер координат вершин vertexBuffer с атрибутом a_vertex a_vertex_Handle, 3, GLES20.GL_FLOAT, false, 0,vertexBuffer); //метод, который связывает буфер координат векторов нормалей normalBuffer с атрибутом a_normal public void linkNormalBuffer (FloatBuffer normalBuffer){ a_normal_Handle, 3, GLES20.GL_FLOAT, false, 0,normalBuffer); //метод, который связывает буфер цветов вершин colorBuffer с атрибутом a_color public void linkColorBuffer (FloatBuffer colorBuffer){ a_color_Handle, 4, GLES20.GL_FLOAT, false, 0, colorBuffer);
// modelViewProjectionMatrix с униформой u_modelViewProjectionMatrix public void linkModelViewProjectionMatrix (float [] modelViewProjectionMatrix){ GLES20.glGetUniformLocation(program_Handle, "u_modelViewProjectionMatrix"); //с униформой u_modelViewProjectionMatrix GLES20.glUniformMatrix4fv( u_modelViewProjectionMatrix_Handle, 1, false, modelViewProjectionMatrix, 0); // метод, который связывает координаты камеры с униформой u_camera // метод, который связывает координаты источника света с униформой u_lightPosition GLES20.glUniform3f(u_lightPosition_Handle, xLightPosition, yLightPosition, zLightPosition); // метод, который делает шейдерную программу данного класса активной // конец класса
|