
- •OpenGl es для новичков
- •2.5 OpenGl es 2.0. Урок 5 Шейдер преломления света……………………………………… 60
- •1.1 OpenGl es в Android, класс glSurfaceView
- •1.2 OpenGl es 1. Основы рисования для начинающих
- •Aтрибут координат вершины.
- •Цвет как атрибут вершины.
- •Координаты вектора нормали.
- •1.3 OpenGl es 1. Применение индексов при обходе вершин
- •1.4 OpenGl es 1. Освещение и материалы
- •1.5 OpenGl es 1. Двумерные текстуры
- •1.6 OpenGl es 1. Текстурирование на примере
- •1.7 OpenGl es 1. Как сделать текстуры прозрачными
- •1.8 OpenGl es 1. Как наложить несколько текстур на один полигон
- •1.9 OpenGl es 1. Текстуры в движении
- •1.10 OpenGl es 1. Зеркальное отражение при помощи кубических текстур
- •2.1 OpenGl es 2.0. Урок первый-Шейдеры Введение в шейдеры.
- •Получение матриц.
- •Создание шейдерного объекта в OpenGl es 2.0
- •Передача данных униформ в шейдеры.
- •Передача атрибутов вершин в вершинный шейдер.
- •Класс шейдерного объекта.
- •2.2 OpenGl es 2.0. Урок второй - Освещение в шейдере
- •Фоновое (ambient) освещение.
- •Диффузное (diffuse) или рассеянное освещение.
- •Зеркальное (specular) или бликовое освещение.
- •Коды шейдеров.
- •Практика. Освещение плоской поверхности.
- •2.3 OpenGl es 2.0. Урок третий - Двумерные текстуры Загрузка текстуры из файла.
- •Доступ к текстурам из шейдера.
- •Получение цвета пикселя из текстуры.
- •Практика. Текстурированный квадрат.
- •Включаем свет.
- •2.4 OpenGl es 2.0. Урок 4. Гладкие поверхности. Полигональная сетка.
- •Расчет нормалей.
- •Разбиваем сетку на треугольники и рисуем поверхность.
- •Полный код класса рендерера, рисующего поверхность.
- •2.5 OpenGl es 2.0. Урок 5. Шейдер преломления света
Коды шейдеров.
Объединим полученные знания и напишем коды шейдеров для освещения.
Код вершинного шейдера:
// принимаем матрицу модели-вида-проекции
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 нам потребовалось бы разбить квадрат на тысячи вершин. В нашем примере потребовалось всего четыре вершины.