Студопедия — OpenGL ES 2.0. Урок второй - Освещение в шейдере
Студопедия Главная Случайная страница Обратная связь

Разделы: Автомобили Астрономия Биология География Дом и сад Другие языки Другое Информатика История Культура Литература Логика Математика Медицина Металлургия Механика Образование Охрана труда Педагогика Политика Право Психология Религия Риторика Социология Спорт Строительство Технология Туризм Физика Философия Финансы Химия Черчение Экология Экономика Электроника

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






В отличие от OpenGL ES 1 в OpenGL ES 2.0 не предусмотрено специальных команд (glLightfv и glMaterialfv) для управления освещением и материалами.Вместе с тем,OpenGL ES 2.0 обладает более широкими возможностями по сравнению с OpenGL ES1.

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 нам потребовалось бы разбить квадрат на тысячи вершин. В нашем примере потребовалось всего четыре вершины.

 







Дата добавления: 2015-10-19; просмотров: 624. Нарушение авторских прав; Мы поможем в написании вашей работы!



Обзор компонентов Multisim Компоненты – это основа любой схемы, это все элементы, из которых она состоит. Multisim оперирует с двумя категориями...

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

Важнейшие способы обработки и анализа рядов динамики Не во всех случаях эмпирические данные рядов динамики позволяют определить тенденцию изменения явления во времени...

ТЕОРЕТИЧЕСКАЯ МЕХАНИКА Статика является частью теоретической механики, изучающей условия, при ко­торых тело находится под действием заданной системы сил...

Кишечный шов (Ламбера, Альберта, Шмидена, Матешука) Кишечный шов– это способ соединения кишечной стенки. В основе кишечного шва лежит принцип футлярного строения кишечной стенки...

Принципы резекции желудка по типу Бильрот 1, Бильрот 2; операция Гофмейстера-Финстерера. Гастрэктомия Резекция желудка – удаление части желудка: а) дистальная – удаляют 2/3 желудка б) проксимальная – удаляют 95% желудка. Показания...

Ваготомия. Дренирующие операции Ваготомия – денервация зон желудка, секретирующих соляную кислоту, путем пересечения блуждающих нервов или их ветвей...

Тема: Составление цепи питания Цель: расширить знания о биотических факторах среды. Оборудование:гербарные растения...

В эволюции растений и животных. Цель: выявить ароморфозы и идиоадаптации у растений Цель: выявить ароморфозы и идиоадаптации у растений. Оборудование: гербарные растения, чучела хордовых (рыб, земноводных, птиц, пресмыкающихся, млекопитающих), коллекции насекомых, влажные препараты паразитических червей, мох, хвощ, папоротник...

Типовые примеры и методы их решения. Пример 2.5.1. На вклад начисляются сложные проценты: а) ежегодно; б) ежеквартально; в) ежемесячно Пример 2.5.1. На вклад начисляются сложные проценты: а) ежегодно; б) ежеквартально; в) ежемесячно. Какова должна быть годовая номинальная процентная ставка...

Studopedia.info - Студопедия - 2014-2024 год . (0.009 сек.) русская версия | украинская версия