8 октября 2012 г.

OpenGL ES 2.0. Урок второй-Освещение в шейдере

В отличие от OpenGL ES 1 в OpenGL ES 2.0 не предусмотрено специальных команд (glLightfv и glMaterialfv) для управления освещением и материалами. Вместе с тем,  OpenGL ES 2.0 обладает более широкими возможностями по сравнению с OpenGL ES 1.
OpenGL ES 1 автоматически рассчитывает освещение вершин и далее интерполирует их освещенность в цвет пикселя. Вмешаться в процесс расчета цвета пикселя в классическом OpenGL ES 1 мы не можем. Поэтому, чтобы получить плавные переходы в освещенности объекта нужно увеличивать количество вершин. В OpenGL ES 2.0 все проще. Расчет освещенности можно перенести в фрагментный шейдер, в котором все операции производятся с конкретным пикселем.
Поговорим немного об видах освещения.
Фоновое (ambient) освещение.
Фоновое освещение освещает объекты одинаково со всех сторон. Оно не зависит от положения источника света и глаза наблюдателя. Задается константой.
Диффузное (diffuse) или рассеянное освещение.
Яркость объекта, освещенного диффузным светом, зависит от положения объекта и от положения источника света. Диффузный свет отражается от поверхности одинаково во все  стороны. Поэтому положение глаза наблюдателя на диффузное освещение не влияет. Яркость диффузного освещения определяют по фактору Ламберта. Вычисляется косинус угла между вектором нормали и вектором, указывающим из точки на источник света. Чем этот угол меньше, тем ярче освещена точка. Если угол = 0, получаем максимальную яркость. Если угол=90 градусов - яркость будет равна нулю. Покажу это на рисунке: 


Косинус угла между векторами равен скалярному произведению двух векторов единичной длины. В фрагментном шейдере у нас уже есть нормализованный вектор нормали для данного пикселя:
vec3 n_normal=normalize(v_normal); 
Также во фрагментном шейдере мы имеем интерполированное для каждого пикселя значение координат точки поверхности v_vertex. Передадим координаты источника света как униформу во фрагментный шейдер:
uniform vec3 u_lightPosition;
Теперь мы можем вычислить вектор из точки поверхности на источник света и нормализовать его:
vec3 lightvector = normalize(u_lightPosition - v_vertex); 
Вычислим скалярное произведение вектора нормали n_normal и вектора lightvector:
dot(n_normal, lightvector)
В общем случае скалярное произведение может быть отрицательным, если угол между векторами больше 90 градусов. Отрицательные значения нам нужно отсечь. Поэтому выберем максимальное значение между скалярным произведением и нулем:
max(dot(n_normal, lightvector), 0.0)
Умножим полученное значение на некоторый коэффициент диффузного освещения  k_diffuse и получим яркость диффузного освещения пикселя:
float diffuse = k_diffuse * max(dot(n_normal, lightvector), 0.0);
Кстати, число с плавающей точкой в шейдерах нужно указывать как 0.0. Если указать просто 0, то будет ошибка и шейдер не скомпилируется.

Зеркальное (specular) или бликовое освещение.
Диффузное освещение не зависит от положения глаза наблюдателя (камеры). Зеркальное освещение определяется долей отраженной световой энергии попавшей в камеру. Поэтому яркость зеркального освещения зависит не только от положения источника света, но также и от положения камеры. Существует много моделей зеркального освещения. Рассмотрим наиболее распространенную из них - модель Фонга. Вычисляем вектор отраженного луча света от точки, далее находим косинус угла между отраженным вектором и направлением на камеру. Чем меньше угол, тем больше косинус и тем больше света попадет в камеру. Максимальная яркость достигается при угле равном нулю, минимальная при угле 90 градусов. Смотрите на рисунок:
При расчете диффузного освещения мы определили вектор единичной длины, проходящий из освещаемой точки к  источнику света и назвали его lightvector. Очевидно, что вектор падающего луча света нужно провести от источника света к точке на поверхности, т.е. просто поменять знак на минус. Для вычисления отраженного вектора в GLSL существует специальная функция reflect:
vec3 reflectvector = reflect(-lightvector, n_normal);
Передадим координаты камеры в фрагментный шейдер как униформу u_camera:
uniform vec3 u_camera; 
Теперь вычислим вектор, указывающий из точки освещения на камеру и нормализуем его:
vec3 lookvector = normalize(u_camera - v_vertex);
Далее нам нужно вычислить косинус угла между отраженным вектором и направлением на камеру. Это скалярное произведение двух единичных векторов:
dot(lookvector,reflectvector)
Отсекаем отрицательные значения скалярного произведения при помощи функции max:
max(dot(lookvector,reflectvector),0.0)
При отражении света на поверхности появляются блики. Размер блика можно регулировать при помощи параметра блеска. Вычисленное значение скалярного произведения нужно возвести в степень блеска. Для возведения в степень в GLSL предусмотрена функция pow Обычно значение блеска выбирают в размере несколько десятков. При увеличении блеска размер блика уменьшается, но яркость его увеличивается. И наоборот, чем меньше блеск, тем больше размер блика, но яркость его становится меньше. Пусть, для примера, блеск будет равен 40. Возведем полученное скалярное произведение в степень 40:
pow( max(dot(lookvector,reflectvector),0.0), 40.0 )
Умножим полученное значение на коэффициент зеркального освещения k_specular и получим яркость зеркального освещения для данного пикселя:
float specular = k_specular * pow( max(dot(lookvector,reflectvector),0.0), 40.0 );

Для того, чтобы получить цвет пикселя с учетом освещения нужно сложить фоновую диффузную и зеркальную части освещения ambient + diffuse + specular и умножить на вектор цвета пикселя, полученного при интерполяции цветов вершин v_color.
gl_FragColor = (ambient+diffuse+specular)*v_color;
Если мы не хотим разукрашивать пиксели интерполированными цветами вершин достаточно определить вектор белого цвета:
vec4 one=vec4(1.0,1.0,1.0,1.0);
и умножить на него яркость освещения:
gl_FragColor = (ambient+diffuse+specular)*one;
Для начала рассмотрим этот черно-белый вариант освещения.


Коды шейдеров.
Объединим полученные знания и напишем коды шейдеров для освещения.
Код вершинного шейдера:
// принимаем матрицу модели-вида-проекции
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;
//принимаем координаты камеры
uniform vec3 u_camera;
//принимаем координаты источника света
uniform vec3 u_lightPosition;
//принимаем координаты пикселя для поверности после интерполяции
varying vec3 v_vertex;
//принимаем вектор нормали для пикселя после интерполяции
varying vec3 v_normal;
//принимаем цвет пикселя после интерполяции
varying vec4 v_color;
void main() {
        //повторно нормализуем нормаль пикселя, 
        //т.к. при интерполяции нормализация может нарушиться
        vec3 n_normal=normalize(v_normal);
       //вычисляем единичный вектор, указывающий из пикселя на источник света
        vec3 lightvector = normalize(u_lightPosition - v_vertex);
       //вычисляем единичный вектор, указывающий из пикселя на камеру
       vec3 lookvector = normalize(u_camera - v_vertex);
       //определяем яркость фонового освещения
       float ambient=0.2;
       //определяем коэффициент диффузного освещения
       float k_diffuse=0.8;
       //определяем коэффициент зеркального освещения
       float k_specular=0.4;
       //вычисляем яркость диффузного освещения пикселя
       float diffuse = k_diffuse * max(dot(n_normal, lightvector), 0.0);
       //вычисляем вектор отраженного луча света 
       vec3 reflectvector = reflect(-lightvector, n_normal);
       //вычисляем яркость зеркального освещения пикселя
       float specular = k_specular * pow( max(dot(lookvector,reflectvector),0.0), 40.0 );
      //определяем вектор белого цвета
      vec4 one=vec4(1.0,1.0,1.0,1.0);
      //вычисляем цвет пикселя
      gl_FragColor = (ambient+diffuse+specular)*one;
};

Практика. Освещение плоской поверхности.
OpenGL ES 2.0 поддерживается ОС "Android" начиная с версии 2.2.  
Перейдем к практике. Чтобы подготовить программу к работе с OpenGL ES 2.0 нужно внести изменения в файл манифеста, а именно в секцию uses-feature добавить строку: 
android:glEsVersion="0x00020000". Например, так:
...........................
<uses-sdk
    android:minSdkVersion="8"
    android:targetSdkVersion="15" />
<uses-feature android:glEsVersion="0x00020000" />
    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
...........................
Кроме того, в конструкторе собственного класса, расширяющего класс GLSurfaceView нужно обязательно прописать команду setEGLContextClientVersion(2). Пример:
//Опишем наш класс MyClassSurfaceView расширяющий GLSurfaceView
public class MyClassSurfaceView extends GLSurfaceView{
        //создадим ссылку для хранения экземпляра нашего класса рендерера
        private MyClassRenderer renderer;
        // конструктор
        public MyClassSurfaceView(Context context) {
                // вызовем конструктор родительского класса GLSurfaceView
                super(context);
                // установим версию OpenGL ES 2.0
                setEGLContextClientVersion(2);
                // создадим экземпляр нашего класса MyClassRenderer
                renderer = new MyClassRenderer(context);
               // запускаем рендерер
               setRenderer(renderer);
               // установим режим циклического запуска метода onDrawFrame 
               setRenderMode(GLSurfaceView.RENDERMODE_CONTINUOUSLY);
               // при этом запускается отдельный поток
               // в котором циклически вызывается метод onDrawFrame
               // т.е. бесконечно происходит перерисовка кадров
        }
}
Чтобы не усложнять урок будем рисовать квадрат, расположенный в плоскости XZ:
Поместим источник света над началом координат. У квадрата четыре вершины, поэтому его можно разбить на два треугольника и рисовать при помощи правила обхода GL_TRIANGLE_STRIP. Обход вершин в треугольниках выполняется против часовой стрелки для того, чтобы нормаль вершин была направлена вверх вдоль оси Y:
Напишем рабочий код для рисования квадрата с освещением. Напомню, что класс Shader был описан в первом уроке. Отмечу, что при создании экрана шейдерные объекты разрушаются. Поэтому объект класса Shader должен создаваться в методе onSurfaceCreated.

public class MyClassRenderer implements GLSurfaceView.Renderer{
        // интерфейс GLSurfaceView.Renderer содержит
        // три метода onDrawFrame, onSurfaceChanged, onSurfaceCreated
        // которые должны быть переопределены
        // текущий контекст
        private Context context;
        //координаты камеры
        private float xСamera, yCamera, zCamera;
        //координаты источника света
        private float xLightPosition, yLightPosition, zLightPosition;
        //матрицы
        private float[] modelMatrix;
        private float[] viewMatrix;
        private float[] modelViewMatrix;
        private float[] projectionMatrix;
        private float[] modelViewProjectionMatrix;
        //буфер для координат вершин
        private FloatBuffer vertexBuffer;
        //буфер для нормалей вершин
        private FloatBuffer normalBuffer;
        //буфер для цветов вершин
        private FloatBuffer colorBuffer;
        //шейдерный объект
        private Shader mShader;

        //конструктор
        public MyClassRenderer(Context context) {
                // запомним контекст
                // он нам понадобится в будущем для загрузки текстур
                this.context=context;
                //координаты точечного источника света
                xLightPosition=0;
                yLightPosition=0.6f;
                zLightPosition=0;
                //матрицы
                modelMatrix=new float[16];
                viewMatrix=new float[16];
                modelViewMatrix=new float[16];
                projectionMatrix=new float[16];
                modelViewProjectionMatrix=new float[16];
                //мы не будем двигать объекты
                //поэтому сбрасываем модельную матрицу на единичную
                Matrix.setIdentityM(modelMatrix, 0);
                //координаты камеры
                xСamera=0.6f;
                yCamera=3.4f;
                zCamera=3f;
                //пусть камера смотрит на начало координат
                //и верх у камеры будет вдоль оси Y
                //зная координаты камеры получаем матрицу вида

                Matrix.setLookAtM(
                                  viewMatrix, 0, xСamera, yCamera, zCamera, 0, 0, 0, 0, 1, 0);
                // умножая матрицу вида на матрицу модели
                // получаем матрицу модели-вида
                Matrix.multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0);
               //координаты вершины 1
               float x1=-2;
                float y1=0;
                float z1=-2;
               //координаты вершины 2
                float x2=-2;
                float y2=0;
                float z2=2;
               //координаты вершины 3
                float x3=2;
                float y3=0;
                float z3=-2;
               //координаты вершины 4
                float x4=2;
                float y4=0;
                float z4=2;
                //запишем координаты всех вершин в единый массив
                float vertexArray [] = {x1,y1,z1, x2,y2,z2, x3,y3,z3, x4,y4,z4};
                //создадим буфер для хранения координат вершин
                ByteBuffer bvertex = ByteBuffer.allocateDirect(vertexArray.length*4);
                bvertex.order(ByteOrder.nativeOrder());
                vertexBuffer = bvertex.asFloatBuffer();
                vertexBuffer.position(0);
                //перепишем координаты вершин из массива в буфер
                vertexBuffer.put(vertexArray);
                vertexBuffer.position(0);
                //вектор нормали перпендикулярен плоскости квадрата
                //и направлен вдоль оси Y
                float nx=0;
                float ny=1;
                float nz=0;
                //нормаль одинакова для всех вершин квадрата,
                //поэтому переписываем координаты вектора нормали в массив 4 раза
                float normalArray [] ={nx, ny, nz,   nx, ny, nz,   nx, ny, nz,   nx, ny, nz};
               //создадим буфер для хранения координат векторов нормали
                ByteBuffer bnormal = ByteBuffer.allocateDirect(normalArray.length*4);
                bnormal.order(ByteOrder.nativeOrder());
                normalBuffer = bnormal.asFloatBuffer();
                normalBuffer.position(0);
               //перепишем координаты нормалей из массива в буфер
                normalBuffer.put(normalArray);
                normalBuffer.position(0);
               //разукрасим вершины квадрата, зададим цвета для вершин
               //цвет первой вершины - красный
                float red1=1;
                float green1=0;
                float blue1=0;
               //цвет второй вершины - зеленый
                float red2=0;
                float green2=1;
                float blue2=0;
                //цвет третьей вершины - синий
                float red3=0;
                float green3=0;
                float blue3=1;
                //цвет четвертой вершины - желтый
                float red4=1;
                float green4=1;
                float blue4=0;
               //перепишем цвета вершин в массив
               //четвертый компонент цвета (альфу) примем равным единице
                float colorArray [] = {
                                red1, green1, blue1, 1,
                                red2, green2, blue2, 1,
                                red3, green3, blue3, 1,
                                red4, green4, blue4, 1,
                                };
                //создадим буфер для хранения цветов вершин
                ByteBuffer bcolor = ByteBuffer.allocateDirect(colorArray.length*4);
                bcolor.order(ByteOrder.nativeOrder());
                colorBuffer = bcolor.asFloatBuffer();
                colorBuffer.position(0);
                //перепишем цвета вершин из массива в буфер
                colorBuffer.put(colorArray);
                colorBuffer.position(0);
        }//конец конструктора

        //метод, который срабатывает при изменении размеров экрана
        //в нем мы получим матрицу проекции и матрицу модели-вида-проекции
        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);
                // матрица проекции изменилась,
                // поэтому нужно пересчитать матрицу модели-вида-проекции
                Matrix.multiplyMM(
                        modelViewProjectionMatrix, 0, projectionMatrix, 0, modelViewMatrix, 0);
        }
         
        //метод, который срабатывает при создании экрана
        //здесь мы создаем шейдерный объект
        public void onSurfaceCreated(GL10 unused, EGLConfig config) {
                //включаем тест глубины
                GLES20.glEnable(GLES20.GL_DEPTH_TEST);
                //включаем отсечение невидимых граней
                GLES20.glEnable(GLES20.GL_CULL_FACE);
                //включаем сглаживание текстур, это пригодится в будущем
                GLES20.glHint(
                           GLES20.GL_GENERATE_MIPMAP_HINT, GLES20.GL_NICEST);
                //записываем код вершинного шейдера в виде строки
                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;"+
                "uniform vec3 u_camera;"+
                "uniform vec3 u_lightPosition;"+
                "varying vec3 v_vertex;"+
                "varying vec3 v_normal;"+
                "varying vec4 v_color;"+
                "void main() {"+
                     "vec3 n_normal=normalize(v_normal);"+
                     "vec3 lightvector = normalize(u_lightPosition - v_vertex);"+
                     "vec3 lookvector = normalize(u_camera - v_vertex);"+
                     "float ambient=0.2;"+
                     "float k_diffuse=0.8;"+
                     "float k_specular=0.4;"+
                     "float diffuse = k_diffuse * max(dot(n_normal, lightvector), 0.0);"+
                     "vec3 reflectvector = reflect(-lightvector, n_normal);"+
                     "float specular = k_specular * pow( max(dot(lookvector,reflectvector),0.0), 40.0 );"+
                     "vec4 one=vec4(1.0,1.0,1.0,1.0);"+
                     "gl_FragColor = (ambient+diffuse+specular)*one;"+
                "}";
                //создадим шейдерный объект 
                mShader=new Shader(vertexShaderCode, fragmentShaderCode);
                //свяжем буфер вершин с атрибутом a_vertex в вершинном шейдере
                mShader.linkVertexBuffer(vertexBuffer);
                //свяжем буфер нормалей с атрибутом a_normal в вершинном шейдере
                mShader.linkNormalBuffer(normalBuffer);
                //свяжем буфер цветов с атрибутом a_color в вершинном шейдере
                mShader.linkColorBuffer(colorBuffer);
                //связь атрибутов с буферами сохраняется до тех пор,
                //пока не будет уничтожен шейдерный объект
       }

        //метод, в котором выполняется рисование кадра
        public void onDrawFrame(GL10 unused) {
                //очищаем кадр
                GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
                     //в отличие от атрибутов связь униформ с внешними параметрами
                //не сохраняется, поэтому перед рисованием каждого кадра
                //нужно связывать униформы заново
                //передаем в шейдерный объект матрицу модели-вида-проекции 
                mShader.linkModelViewProjectionMatrix(modelViewProjectionMatrix);
                //передаем в шейдерный объект координаты камеры
                mShader.linkCamera(xСamera, yCamera, zCamera);
               //передаем в шейдерный объект координаты источника света
                mShader.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
               //делаем шейдерную программу активной
                mShader.useProgram();
                //рисуем квадрат
                GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
                //последний аргумент в этой команде - это количество вершин =4
        }
}//конец класса
В запустим программу на исполнение и получим следующую картинку:
Диффузное и зеркальное освещение

Изменив код фрагментного шейдера можно выделить отдельно диффузное освещение:
gl_FragColor = (ambient+diffuse)*one;
и зеркальное освещение:
gl_FragColor = (ambient+specular)*one;
Диффузное освещение
Зеркальное освещение

Теперь скомбинируем освещение с интерполированными цветами вершин:
gl_FragColor = (ambient+diffuse+specular)*v_color;

Получилось слишком тускло. Это происходит потому, что мы умножаем яркость освещения на на каждую компоненту вектора цвета пикселя (красную, зеленую и синюю). Произведение двух чисел, каждое из которых меньше единицы, дает число меньшее обоих множителей и поэтому общая яркость цвета уменьшается. Можно комбинировать освещение с цветом по другому. В GLSL есть функция смешивания mix, которая выглядит так:
mix(a,b,k)=a*(1-k)+b*k
где a и b - аргументы которые нужно смешать, а k-коэффициент смешивания, который меняется от 0 до 1. При k=0 результат функции будет равен аргументу a, при k=1 - аргументу b.
Определим во фрагментном шейдере для освещения свой вектор цвета:
vec4 lightColor = (ambient+diffuse+specular)*one;
и смешаем его наполовину с цветом пикселя:  
gl_FragColor = mix(lightColor, v_color, 0.5);
Результат будет выглядеть так:

Для получения такого качественного освещения в классическом OpenGL ES 1 нам потребовалось бы разбить квадрат на тысячи вершин. В нашем примере потребовалось всего четыре вершины.
На этом наш второй урок закончен. Полный код урока можно скачать отсюда Ссылка.
Ждите следующих уроков.

36 комментариев:

  1. Отличная статья :) Не все понятно с первого раза но в целом после третьего прочтения смог разобраться...
    Непонятно с умножением матриц, Насколько я понимаю последовательность ведь важна. По этому неплохо бы еще осветить и этот момент.
    С нетерпением жду следующих статей!

    ОтветитьУдалить
    Ответы
    1. По поводу матриц. Прикрепим к камере свою систему координат. Вначале камера находится в точке 0,0,0 мировых координат. На этом этапе координаты вершины в мировой системе и в системе координат камеры совпадают. Начинаем крутить модель. Чтобы получить новые координаты вершины после поворота модели нужно их помножить слева на матрицу модели:
      vertex1=modelMatrix * vertex0
      При этом камера еще находится в точке 0,0,0 и координаты вершины в обоих системах совпадают.
      Теперь перемещаем камеру из начала мировых координат. Относительно камеры вершина совершает еще одно движение. Можно получить новые координаты вершины в координатной системе камеры:
      vertex2=viewMatrix * vertex1
      Далее нужно подправить координаты с учетом их искажения перспективной проекцией:
      vertex3=projectionMatrix * vertex2
      Итого получаем:
      vertex3 = projectionMatrix * viewMatrix * modelMatrix * vertex0
      И все это для того чтобы получить gl_Position

      Удалить
  2. не совсем понятно, что здесь используется вместо glMatrixMode,glFrustum/gluPerspective. или все матрицы устанавливаются в шейдере?

    ОтветитьУдалить
    Ответы
    1. В отличие от OpenGL ES 1.0 в OpenGL ES 2.0 никаких встроенных в графический конвейер матриц не существует. Поэтому команды glMatrixMode, glFrustum, gluPerspective не используются. Все необходимые матрицы мы создаем сами. Существуют функции, облегчающие их получение. Зная координаты и направление камеры мы можем получить матрицу вида при помощи функции Matrix.setLookAtM. Зная плоскости отсечения перспективной проекции мы можем получить матрицу проекции при помощи функции Matrix.frustumM. В классе android.opengl.Matrix есть много функций позволяющих получить матрицу модели после поворотов или перемещений объекта. Умножая слева направо матрицы проекции, вида и модели получаем матрицу модели-вида-проекции, которую затем передаем в шейдер как униформу. Прочитайте первый урок по OpenGL ES 2.0.

      Удалить
    2. Большое спасибо. Я читал урок, просто показалось странным отсутствие этих команд.

      Удалить
  3. есть, допустим мы рассчитали, что блик (спекулярная составляющая) должен быть в левом нижнем углу. То есть при повороте квадрата блик должен оставаться там, однако получается, что он привязывается к точке на квадрате. Аналогично и с диффузной составляющей. Подскажите пожалуйста, почему?

    ОтветитьУдалить
  4. извиняюсь, начало предыдущего сообщения
    я использую команду matrix.rotatem. квадрат поворачивается как надо, а с освещением проблемы.

    ОтветитьУдалить
    Ответы
    1. Освещение зависит от вектора нормали. Если вы повернули вершины, то вектор нормали тоже повернется. Нужно пересчитать нормаль после поворота.

      Удалить
    2. Поворот вершин и нормали проще сделать в вершинном шейдере. В методе onDrawFrame поворачиваете матрицу модели, которую передаете в вершинный шейдер как отдельную униформу
      uniform mat4 u_modelMatrix;
      ......
      attribute vec3 a_vertex;
      attribute vec3 a_normal;
      varying vec3 v_vertex;
      varying vec3 v_normal;
      void main(){
      //поворачиваем вершину
      v_vertex=vec3(u_modelMatrix*vec4(a_vertex,1.0));
      // нормализуем нормаль
      vec3 n_normal=normalize(a_normal);
      //поворачиваем нормаль
      v_normal=vec3(u_modelMatrix*vec4(n_normal,1.0));
      //далее во фрагментном шейдере
      //для расчета освещения будут использованы
      //повернутая вершина и нормаль
      ......
      }

      Удалить
    3. И еще не забудьте в методе onDrawFrame пересчитать матрицу модели-вида-проекции после поворота матрицы модели.

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

      Удалить
    5. А подойдет ли такой вариант если на модель вращается с текстурой?

      Удалить
    6. Если привязать координаты текстуры к вершинам модели, то текстура будет вращаться вместе с моделью

      Удалить
    7. А можно подробнее? Если не затруднит! Заранее спасибо!

      Удалить
    8. Рассмотрим простой случай. Пусть текстура относительно вершин объекта неподвижна, т.е. наклеена на объект как обои. Это означает, что при вращении объекта, координаты текстуры не меняются. Создаем постоянный массив координат текстур для вершин, переводим его в байтовый буфер и передаем в вершинный шейдер как атрибут attribute vec2 a_texcoord. Далее отправляем a_texcoord на интерполяцию
      v_texcoord=a_texcoord.
      Во фрагментом шейдере вычисляем цвет текстуры
      vec4 textureColor=texture2D(u_texture, v_texcoord)

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

      Удалить
    10. Огромное спасибо! Все получилось! Еще вопрос. А как сделать у текстуры прозрачным какой нибудь цвет (черный)?

      Удалить
    11. Например так:
      float arg=step(0.1,length(color2));
      gl_FragColor=mix(color1, color2, arg);

      если общая яркость цвета2 length(color2)<0.1, то arg=0.0
      при этом gl_FragColor=color1

      если общая яркость цвета2 length(color2)>0.1, то arg=1.0
      при этом gl_FragColor=color2

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

      Удалить
  5. большое спасибо! все получилось!

    ОтветитьУдалить
  6. Андрей на основе вашего примера хотел вывести на экран две разные плоскости, добавил шейдерный объект:
    mShader=new Shader(vertexShaderCode, fragmentShaderCode);
    mShader1=new Shader(vertexShaderCode, fragmentShaderCode);
    //Связываю буферы вершин
    mShader.linkVertexBuffer(vertexBuffer); //рисует первую плоскость
    mShader1.linkVertexBuffer(vertexBuffer1);
    //первая пропадает, рисуется вторая с данными первой

    Получается, что program_Handle при создании нового шейдерного объекта не изменился. Как правильно вывести фигуры?

    ОтветитьУдалить
    Ответы
    1. mShader.linkVertexBuffer(vertexBuffer) сам по себе не рисует ничего. Он просто связывает буфер вершин с соответствующим атрибутом вершинного шейдера в шейдерном объекте. Рисование производится командой glDrawArrays (или glDrawElements). Если в одном кадре рисуются два разных объекта с разными шейдерами нужно действовать так
      mShader.useProgram();
      glDrawArrays(...);
      mShader1.useProgram();
      glDrawArrays(...);
      Это в одном цикле onDrawFrame

      Удалить
    2. Андрей попробовал, не выводит. Выводи ту фигуру, чья запись
      mShader.linkVertexBuffer(vertexBuffer); mShader1.linkVertexBuffer(vertexBuffer1);
      оказалась последней.
      Такая запись onDrawFrame верна?
      public void onDrawFrame(GL10 unused) {
      GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
      mShader.linkModelViewProjectionMatrix (modelViewProjectionMatrix);
      mShader1.linkModelViewProjectionMatrix (modelViewProjectionMatrix1);
      mShader.linkCamera(xСamera, yCamera, zCamera);
      mShader1.linkCamera(xСamera, yCamera, zCamera);
      mShader.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
      mShader1.linkLightSource(xLightPosition, yLightPosition, zLightPosition);

      mShader.useProgram();
      GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

      mShader1.useProgram();
      GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
      }

      Удалить
  7. А если попробовать так:
    public void onDrawFrame(GL10 unused) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

    mShader.useProgram();
    mShader.linkModelViewProjectionMatrix (modelViewProjectionMatrix);
    mShader.linkCamera(xСamera, yCamera, zCamera);
    mShader.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
    mShader.linkVertexBuffer(vertexBuffer);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

    mShader1.useProgram();
    mShader1.linkModelViewProjectionMatrix(modelViewProjectionMatrix1);
    mShader1.linkCamera(xСamera, yCamera, zCamera);
    mShader1.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
    mShader1.linkVertexBuffer(vertexBuffer1);
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
    }
    то что при этом нарисуется ?

    ОтветитьУдалить
  8. Так выводит обе плоскости. Вроде разные объекты, а ещё нужно соблюдать порядок.
    Спасибо за ответ.

    ОтветитьУдалить
  9. Анонимный17 июня 2013 г., 8:00

    Здравствуйте!

    У меня не получается сделать так, чтобы куб стал вращаться по нажатию на экране пальцем.

    Я делаю в onDrawFrame
    public void onDrawFrame(GL10 unused) {
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
    mShader.linkModelViewProjectionMatrix(modelViewProjectionMatrix);
    mShader.linkCamera(xCamera, yCamera, zCamera);
    mShader.linkLightSource(xLightPosition, yLightPosition, zLightPosition);
    mShader.useProgram();

    Matrix.setIdentityM(modelMatrix, 0);
    Matrix.setIdentityM(mCurrentRotation, 0);
    Matrix.rotateM(mCurrentRotation, 0, angleX, 0.0f, 1.0f, 0.0f);
    Matrix.rotateM(mCurrentRotation, 0, angleY, 1.0f, 0.0f, 0.0f);

    Matrix.multiplyMM(mTemporaryMatrix, 0, mCurrentRotation, 0, mAccumulatedRotation, 0);
    System.arraycopy(mTemporaryMatrix, 0, mAccumulatedRotation, 0, 16);

    Matrix.multiplyMM(mTemporaryMatrix, 0, modelMatrix, 0, mAccumulatedRotation, 0);
    System.arraycopy(mTemporaryMatrix, 0, modelMatrix, 0, 16);

    unused.glEnable(GL10.GL_CULL_FACE);
    unused.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    unused.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);

    for(int i = 0; i < 6; i++)
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, i*4, 4);

    unused.glDisableClientState(GL10.GL_VERTEX_ARRAY);
    unused.glDisable(GL10.GL_CULL_FACE);

    angleX += speedX;
    angleY += speedY;
    }

    Здесь я пытаюсь повернуть куб:
    Matrix.setIdentityM(modelMatrix, 0);
    Matrix.setIdentityM(mCurrentRotation, 0);
    Matrix.rotateM(mCurrentRotation, 0, angleX, 0.0f, 1.0f, 0.0f);
    Matrix.rotateM(mCurrentRotation, 0, angleY, 1.0f, 0.0f, 0.0f);

    Matrix.multiplyMM(mTemporaryMatrix, 0, mCurrentRotation, 0, mAccumulatedRotation, 0);
    System.arraycopy(mTemporaryMatrix, 0, mAccumulatedRotation, 0, 16);

    Matrix.multiplyMM(mTemporaryMatrix, 0, modelMatrix, 0, mAccumulatedRotation, 0);
    System.arraycopy(mTemporaryMatrix, 0, modelMatrix, 0, 16);

    angleX, angleY меняются в зависимости от того, где находится палец на экране

    ОтветитьУдалить
  10. Если вы используете OpenGL ES 2.0 работа с интерфейсом GL10 unused не имеет смысла.
    Вы пересчитываете матрицу модели при вращении куба. А где вы ее передаете в вершинный шейдер ? И неполохо было бы сам вершинный шейдер посмотреть.

    ОтветитьУдалить
  11. Анонимный17 июня 2013 г., 8:51

    Здравствуйте!

    Я новичок в этом деле. Поэтому я сделал вершинный шейдер как у Вас:

    String vertexShaderCode=
    "uniform mat4 u_modelMatrix;" +
    "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=vec3(u_modelMatrix*vec4(a_vertex,1.0));"+
    "vec3 n_normal=normalize(a_normal);"+
    "v_normal=vec3(u_modelMatrix*vec4(n_normal,1.0));"+
    "v_color=a_color;"+
    "gl_Position = u_modelViewProjectionMatrix * vec4(a_vertex,1.0);"+
    "}";

    "А где вы ее передаете в вершинный шейдер ?" :
    Нужно пересчитать матрицу модели-вида?
    Matrix.multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0);
    а потом передать в вершинный шейдер новую modelViewMatrix?

    подскажите пожалуйста

    ОтветитьУдалить
    Ответы
    1. В моем примере объект неподвижен, поэтому матрица модели всегда равна единичной матрице и не меняется.
      Существует два способа передачи вращения в шейдер.

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

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

      // здесь считаем матрицу модели при повороте
      ............. это кусок вашего кода
      // передаем матрицу модели в шейдер
      shader.linkModelMatrix(modelMatrix);
      // метод linkModelMatrix описан в конце

      // заполняем матрицу вида
      Matrix.setLookAtM(viewMatrix, 0, xposition, yposition, zposition, xlook, ylook, zlook, xtop, ytop, ztop);


      // получаем матрицу модели-вида
      Matrix.multiplyMM(modelViewMatrix, 0, viewMatrix, 0, modelMatrix, 0);
      // получаем матрицу модели-вида-проекции
      Matrix.multiplyMM(modelViewProjectionMatrix, 0, projectionMatrix, 0, modelViewMatrix, 0);
      //передаем ее в шейдер
      shader.linkModelViewProjectionMatrix(modelViewProjectionMatrix)

      //метод
      public void linkModelMatrix(float [] modelMatrix){
      GLES20.glUseProgram(program_Handle);
      //
      int u_modelMatrix_Handle = GLES20.glGetUniformLocation(program_Handle, "u_modelMatrix");
      GLES20.glUniformMatrix4fv(u_modelMatrix_Handle, 1, false, modelMatrix, 0);

      }

      //конец метода

      //Код шейдера как у меня
      String vertexShaderCode=
      "uniform mat4 u_modelMatrix;" +
      "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=vec3(u_modelMatrix*vec4(a_vertex,1.0));"+
      "vec3 n_normal=normalize(a_normal);"+
      "v_normal=vec3(u_modelMatrix*vec4(n_normal,1.0));"+
      "v_color=a_color;"+
      "gl_Position = u_modelViewProjectionMatrix * vec4(a_vertex,1.0);"+
      "}";

      Удалить
  12. Анонимный17 июня 2013 г., 12:08

    Zdravstvuite!

    Spasibo vse srabotalo!

    Pitaus' nalogit' textury. K sogaleniu ne nakladivaetsia. Ne podskagite v chem oshibka?

    onSurfaceCreated:

    String vertexShaderCode=
    "uniform mat4 u_modelMatrix;" +
    "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;"+
    "varying vec2 v_texcoord;"+
    "attribute vec2 a_texcoord;" +
    "void main() {"+
    "v_vertex=vec3(u_modelMatrix*vec4(a_vertex,1.0));"+
    "vec3 n_normal=normalize(a_normal);"+
    "v_normal=vec3(u_modelMatrix*vec4(n_normal,1.0));"+
    "v_color=a_color;"+
    "v_texcoord=a_texcoord;" +
    "gl_Position = u_modelViewProjectionMatrix * vec4(a_vertex,1.0);"+
    "}";

    String fragmentShaderCode=
    "precision mediump float;"+
    "uniform vec3 u_camera;"+
    "uniform vec3 u_lightPosition;"+
    "uniform sampler2D u_texture;" +
    "varying vec2 v_texcoord;" +
    "varying vec3 v_vertex;"+
    "varying vec3 v_normal;"+
    "varying vec4 v_color;"+
    "void main() {"+
    "vec3 n_normal=normalize(v_normal);"+
    "vec3 lightvector = normalize(u_lightPosition - v_vertex);"+
    "vec3 lookvector = normalize(u_camera - v_vertex);"+
    "float ambient=0.2;"+
    "float k_diffuse=0.8;"+
    "float k_specular=0.4;"+
    "float diffuse = k_diffuse * max(dot(n_normal, lightvector), 0.0);"+
    "vec3 reflectvector = reflect(-lightvector, n_normal);"+
    "float specular = k_specular * pow( max(dot(lookvector,reflectvector),0.0), 40.0 );"+
    "vec4 one=vec4(1.0,1.0,1.0,1.0);"+
    "vec4 lightColor = (ambient+diffuse+specular)*one;"+
    "gl_FragColor = mix(lightColor,v_color,0.5);"+
    "vec4 textureColor=texture2D(u_texture, v_texcoord);" +
    "}";


    mShader=new Shader(vertexShaderCode, fragmentShaderCode);
    mShader.linkVertexBuffer(vertexBuffer);
    mShader.linkNormalBuffer(normalBuffer);
    mShader.linkColorBuffer(colorBuffer);
    mShader.linkTexCoordArray(textureBuffer);

    linkTexCoordArray:


    public void linkTexCoordArray(FloatBuffer textureBuffer)
    {
    GLES20.glUseProgram(program_Handle);
    int a_Coord_Handle = GLES20.glGetAttribLocation(program_Handle, "a_texcoord");

    GLES20.glEnableVertexAttribArray(a_Coord_Handle);
    GLES20.glVertexAttribPointer(a_Coord_Handle, 2, GLES20.GL_FLOAT, false, 0, textureBuffer);
    }

    ОтветитьУдалить
  13. Анонимный17 июня 2013 г., 12:08

    onDrawFrame:

    unused.glBindTexture(GL10.GL_TEXTURE_2D, textures[0]);

    unused.glEnableClientState(GL10.GL_VERTEX_ARRAY);
    unused.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);

    unused.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
    unused.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);

    for(int i = 0; i < 6; i++)
    GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, i*4, 4);


    onSurfaceCreated:

    loadTexture(context);

    loadTexture:

    public void loadTexture(Context context)
    {
    InputStream is = context.getResources().openRawResource(R.drawable.olga);
    Bitmap bitmap = null;
    try {
    bitmap = BitmapFactory.decodeStream(is);
    } finally {
    try {
    is.close();
    is = null;
    } catch (IOException e) {
    }
    }

    GLES20.glGenTextures(1, textures, 0);
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textures[0]);

    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_REPEAT);
    GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_REPEAT);

    GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);

    bitmap.recycle();
    }

    ОтветитьУдалить
    Ответы
    1. Уберите ненужные команды
      unused.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      unused.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      unused.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
      unused.glTexCoordPointer(2, GL10.GL_FLOAT, 0, textureBuffer);
      и внимательно прочитайте "OpenGL ES 2.0. Урок третий-Двумерные текстуры"

      Удалить
  14. Анонимный17 июня 2013 г., 15:00

    Skagite, a kak sdelat' tak, chtobi vershini dvigalis' po sinusoidal'nomy zakonu? s povosh'u vertex shader

    ОтветитьУдалить
    Ответы
    1. Читайте урок "OpenGL ES 2.0. Урок 4. Гладкие поверхности. Полигональная сетка" и "OpenGL ES 2.0. Урок 5. Шейдер преломления света". Там все написано.

      Удалить
  15. Здравствуйте!
    Подскажите, пожалуйста, в чем проблема: при применении перед рисованием Matrix.scaleM к ModelMatrix яркость объекта уменьшается пропорционально тому, на сколько я уменьшаю его. Заранее спасибо!

    ОтветитьУдалить
  16. Здравствуйте.
    Отличный урок. Но есть один вопрос. Как будет выглядеть шейдер и основная программа, если не один, а несколько источников света? Грубо говоря у меня есть объект (комната) и в нем 2 источника направленного света (фонари по углам).
    Перекопал интернет. Нашел эту ссылку.
    http://stackoverflow.com/questions/26213558/opengl-es-2-0-multiple-light-sources-shader-issue
    Сделал по ней, но работает некорректно. У меня яркость освещения увеличивается, а координата света одна получается, вместо двух разных. Где-то я что-то не так делаю.
    Буду признателен, если Вы добавите в свою статью несколько источников света. Это очень актуальная тема. Или здесь в комментариях опишите это.

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