Разработка OpenGL приложения под Android. Часть 3. Освещение.


Доброго времени суток.

Это четвертая часть цикла статей "Разработка OpenGL приложения под Android".

Предыдущие статьи:
Разработка OpenGL приложения под Android. Часть 0. Введение.
Разработка OpenGL приложения под Android. Часть 1. Знакомство.
Разработка OpenGL приложения под Android. Часть 2. Матрицы.

Сегодня мы будем работать с освещением.
Это очень важная тема, умение работать с основами освещения пригодится Вам в большинстве Ваших собственных проектов.

Здесь может возникнуть вопрос зачем что-то менять, если по умолчанию в OpenGL уже выставлен источник света и мы и без того видим наши объекты.

Дело в том, что выставленный по умолчанию свет нельзя редактировать: изменять его цвет, насыщенность, положение и другие параметры. Но самое главное это то, что он всего один. Если Вам, к примеру, понадобится еще свет от ламп или фонарика, то стандартное освещение уже не поможет.
[spoiler]
Другими словами, стандартное освещение отлично подходит для базовой настройки приложения и никуда не годится в реальных условиях.
  • Теперь немного теории.
В OpenGL версии 1.0, которую мы сейчас используем, на сцене могут присутствовать до восьми источников света.
Включение и отключение их производится при помощи команд glEnable() и glDisable() которые принимают в качестве параметра описанный источник(GL_LIGHTi, где i - число от 0 до 7).

Допустим, мы включили источник света, но этого все еще не достаточно. Теперь нам необходимо задать его свойства. В OpenGL это делается при помощи команды glLight().

/*http://esate.ru, Freaky_Brainstorm(Brain Freaky)*/

glLight(GLenum light, GLenum  pname, GLfloat  param);

Параметр light указывает на источник света который мы хотим использовать.

Теперь нужно определиться с типом источника света. В OpenGL их существует три: точечный, направленный и прожектор.

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

Параметр pname команды glLightfv
Значение по умолчаниюКраткий комментарий
(0.0, 0.0, 0.0, 1.0)цвет фонового излучения источника света
(1.0, 1.0, 1.0, 1.0) или(0.0, 0.0, 0.0, 1.0)цвет рассеянного излучения источника света (значение по умолчанию для GL_LIGHT0 - белый, для остальных - черный)
(1.0, 1.0, 1.0, 1.0) или (0.0, 0.0, 0.0, 1.0)цвет зеркального излучения источника света (значение по умолчанию для GL_LIGHT0 - белый, для остальных - черный)
Остальные параметры являются специфическими для каждого типа источников света, описанных ниже.
  • Источники направленного света
Источники света такого типа находится в бесконечности и свет от него распространяется в заданном направлении.
Идеально подходит для создания равномерного освещения.

Хорошим примером источника направленного света может служить Солнце. У источника направленного света, кроме компонент излучения, можно задать только направление.

Параметр pname команды glLightfv
Значение по умолчаниюКраткий комментарий
(0.0, 0.0, 1.0, 0.0)(x, y, z, w) направление источника направленного света
Первые три компоненты (x, y, z) задают вектор направления, а компонента w всегда равна нулю (иначе источник превратится в точечный).
  • Точечные источники света
Точечный источник света расположен в некоторой точке пространства и излучает во всех направлениях.
Т.к. расстояние между источником и освещаемой точкой конечно, то можно задать закон убывания интенсивности излучения с расстоянием.

Стандартные средства OpenGL позволяют задавать такой закон в виде обратно-квадратичной функции от расстояния:
a8de52255273c4831cdfbfdd4805f101.png

Т.е. для точечного источника света, кроме свойств излучения, можно задать ещё четыре параметра:

Параметр pname команды glLightfv
Значение по умолчаниюКраткий комментарий
(0.0, 0.0, 1.0, 0.0)позиция источника света (по умолчанию источник света направленный)
1.0постоянная k_const в функции затухания f(d)
0.0 коэффициент k_linear при линейном члене в функции затухания f(d)
0.0коэффициент k_quadratic при квадрате расстояния в функции затухания f(d)
Как видно из таблицы, по умолчанию, интенсивность света не убывает с расстоянием.

Позиция источника света (в случае направленного источника - направление) задается в текущей модельной системе координат.

Например, после выполнения кода:
/*http://esate.ru, Freaky_Brainstorm(Brain Freaky)*/

glPushMatrix();
glLoadIdentity();
glTranslatef(1.0, 1.0, 1.0);
float light0_position[] = { 0.0, 0.0, 0.0, 1.0 };
glLightfv(GL_LIGHT0, GL_POSITION, light0_position);
glPopMatrix();
нулевой источник света будет расположен в точке (1.0, 1.0, 1.0) во внешней (мировой) системе координат.
  • Прожекторы
Одной из разновидностей точечного источника является прожектор. Для него применимы все параметры, что и для точечного источника, но кроме того прожектор позволяет ограничить распространение света конусом.

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

5a84d8ac6f3cf1af128e8cb2674479ef.png

Параметры, специфические для прожектора:

Параметр pname команды glLightfv
Значение по умолчаниюКраткий комментарий
(0.0, 0.0, -1.0)(x, y, z) - направление прожектора (ось ограничивающего конуса)
180.0угол между осью и стороной конуса (он же половина угла при вершине)
0.0экспонента убывания интенсивности

С теорией мы на этом закончим и начнем писать код.


Для начала нам понадобится проект из прошлого урока.
Мы не будем создавать новый, это нудно и не интересно создавать все сначала.
Мы просто модернизируем старое.
И так, что у нас есть?

А есть у нас три самописных класса: OpenGLRenderer, Object3D и Background.

На данном этапе нужно определить в каком из них нужно прописывать освещение.

Background отпадает сразу. В данный момент он не должен меняться от освещения.
Object3D тоже отпадает. Хотя именно для его отображения мы работаем с освещением, но прописывать свет в этом классе пока нет смысла.

Решено. Остается OpenGLRenderer. Он управляет нашими классами, будет управлять и светом.

Открываем его и редактируем функцию onSurfaceChanged() до следующего вида:
/*http://esate.ru, Freaky_Brainstorm(Brain Freaky)*/

   @Override
   public void onSurfaceChanged(GL10 gl, int width, int height) {
      //Создаем экземпляр классов Background и Object3D
      background = new Background(width, height);
      object3D = new Object3D();

      //Переменная aspect будет хранить в себе значение соотношения сторон
      aspect = (float)width / (float)height;
      //Устанавливаем область отрисовки
      gl.glViewport(0, 0, width, height);

      //Выбираем матрицу проекции
      gl.glMatrixMode(GL10.GL_PROJECTION);
      //Умножаем ее на единичную
      gl.glLoadIdentity();
      //Выбираем матрицу вида
      gl.glMatrixMode(GL10.GL_MODELVIEW);
      //Ее тоже умножаем на единичную
      gl.glLoadIdentity();

      //Это все делается для того, чтобы при изменении размера экрана
      //при повороте устройства, у нас ничего не "сломалось"

      //Устанавливаем параметры света и материала
      float[] matDiffuse    = {1.0f, 1.0f, 1.0f, 1.0f};
      float[] matSpecular    = {1.0f, 1.0f, 1.0f, 1.0f};
      float[] lightDiffuse   = {1.0f, 1.0f, 1.0f, 1.0f};
      float[] lightPosition   = {0.0f, 0.0f, -10f, 1.0f};
      float lightShininess   = 60.0f;
      gl.glMaterialfv(GL10.GL_FRONT, GL10.GL_DIFFUSE, FloatBuffer.wrap(matDiffuse));
      gl.glMaterialfv(GL10.GL_FRONT, GL10.GL_SPECULAR, FloatBuffer.wrap(matSpecular));
      gl.glMaterialf(GL10.GL_FRONT, GL10.GL_SHININESS, lightShininess);
      //Указываем, что параметры необходимо применить
      //к источнику света GL_LIGHT0
      gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_DIFFUSE, FloatBuffer.wrap(lightDiffuse));
      gl.glLightfv(GL10.GL_LIGHT0, GL10.GL_POSITION, FloatBuffer.wrap(lightPosition));
      //Включаем источник света GL_LIGHT0
      gl.glEnable(GL10.GL_LIGHT0);

      //Записываем в переменные значения высоты и ширины экрана
      this.width = (float) width; this.height = (float)height;
   }

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

Хорошо.
Наш следующий шаг - редактирование метода onDrawFrame():
/*http://esate.ru, Freaky_Brainstorm(Brain Freaky)*/

   @Override
   public void onDrawFrame(GL10 gl) {
      //Очищаем буферы цвета и глубины
      gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
      //Выбираем матрицу проекции
      gl.glMatrixMode(GL10.GL_PROJECTION);
      //Умножаем на единичную
      gl.glLoadIdentity();
      //Устанавливаем ортогональную проекцию
      gl.glOrthof(0.0f, this.width, 0.0f, this.height, -1.0f, 1.0f);
      //Отключаем тест глубины
      gl.glDisable(GL10.GL_DEPTH_TEST);
      //Отключаем запись в буфер глубины
      gl.glDepthMask(false);
      //Отключаем освещение
      gl.glDisable(GL10.GL_LIGHTING);
      //Выбираем матрицу вида
      gl.glMatrixMode(GL10.GL_MODELVIEW);
      //Умножаем на единичную
      gl.glLoadIdentity();
      //Начало зоны 2D

      //Задаем цвет фона
      gl.glColor4f(.0f, .0f, .0f, 7.0f);
      //Запускаем прорисовку
      background.Draw(gl);

      //Конец зоны 2D
      //Включаем запись в буфер глубины
      gl.glDepthMask(true);
      //Включаем тест глубины
      gl.glEnable(GL10.GL_DEPTH_TEST);
      //Включаем освещение
      gl.glEnable(GL10.GL_LIGHTING);
      //Выбираем матрицу проекции
      gl.glMatrixMode(GL10.GL_PROJECTION);
      //Умножаем на единичную матрицу
      gl.glLoadIdentity();
      //Устанавливаем область видимости. Об этом ниже
      GLU.gluPerspective(gl, 45.0f, aspect, 0.1f, 1000.0f);
      //Выбираем матрицу вида
      gl.glMatrixMode(GL10.GL_MODELVIEW);
      //Умножаем на единичную матрицу
      gl.glLoadIdentity();
      //Устанавливаем точку и направление камеры
      GLU.gluLookAt(gl, 0.5f, 0.5f, 5.0f, 0.5f, 0.5f, 0.5f, 0.0f, 1.0f, 0.0f);
      //Вращаем объект относительно всех координат
      gl.glRotatef(angle, 1.0f, 1.0f, 1.0f);
      //Начало зоны 3D

      //Задаем цвет объекта
      gl.glColor4f(.1f, .1f, .1f, 1.0f);
      //Запускаем прорисовку
      object3D.Draw(gl);

      //Конец зоны 3D
      //Умножаем на единичную матрицу
      gl.glLoadIdentity();
      //Увеличиваем угол вращения
      angle++;
   }

Здесь мы только добавили четыре строки кода.
Отключили освещение для фона и включили его для объекта.
Добавили вращение объекта.

Плюс мы отредактировали цвет фона и позицию камеры.
Смените эти параметры у себя.

Теперь Вы можете запустить проект и увидите, что наше освещение работает, но на плоскости оно смотрится не интересно.

Преобразуем наш класс Object3D.
Для этого нужно изменить переменную vertices следующим образом:
/*http://esate.ru, Freaky_Brainstorm(Brain Freaky)*/

   private float vertices[] = {
        0.0f,  0.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
        1.0f,  0.0f,  0.0f,
        1.0f,  1.0f,  0.0f,
        1.0f,  0.0f,  1.0f,
        1.0f,  1.0f,  1.0f,
        0.0f,  0.0f,  1.0f,
        0.0f,  1.0f,  1.0f,
        0.0f,  0.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
        0.0f,  0.0f,  1.0f,
        0.0f,  0.0f,  0.0f,
        1.0f,  0.0f,  1.0f,
        1.0f,  0.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
        0.0f,  1.0f,  1.0f,
        1.0f,  1.0f,  0.0f,
        1.0f,  1.0f,  1.0f,
   };

Вот теперь можно запустить проект и полюбоваться.

7f610fc0a6ee18c09b42b431685ad7ae.png

Получилось и хорошо и плохо одновременно.

Хорошо то, что освещение работает, но плохо то, что работает не корректно.

Мы вращаем матрицу и источник света вращается вместе с объектом, но это нам подходит сейчас. Мы сможем рассмотреть как освещается объект со всех сторон.

Самое плохое это то, что мы не видим граней, но это легко исправить с помощью нормалей.

Приведем наш класс Object3D к следующему виду:
/*http://esate.ru, Freaky_Brainstorm(Brain Freaky)*/

package com.freaky_brainstorm.oglpart3;

import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.opengles.GL10;

public class Object3D {

   private FloatBuffer vertexBuffer, normalsBuffer;

   private float vertices[] = {
        0.0f,  0.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
        1.0f,  0.0f,  0.0f,
        1.0f,  1.0f,  0.0f,
        1.0f,  0.0f,  1.0f,
        1.0f,  1.0f,  1.0f,
        0.0f,  0.0f,  1.0f,
        0.0f,  1.0f,  1.0f,
        0.0f,  0.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
        0.0f,  0.0f,  1.0f,
        0.0f,  0.0f,  0.0f,
        1.0f,  0.0f,  1.0f,
        1.0f,  0.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
        0.0f,  1.0f,  1.0f,
        1.0f,  1.0f,  0.0f,
        1.0f,  1.0f,  1.0f,
   };

   private float normals[] = {
        0.0f,  0.0f,  1.0f,
        0.0f,  0.0f,  1.0f,
        1.0f,  0.0f,  1.0f,
        1.0f,  0.0f,  1.0f,
        1.0f,  0.0f,  -1.0f,
        1.0f,  0.0f,  -1.0f,
        -1.0f,  0.0f,  -1.0f,
        -1.0f,  0.0f,  -1.0f,
        -1.0f,  0.0f,  0.0f,
        -1.0f,  0.0f,  0.0f,
        -1.0f,  -1.0f,  0.0f,
        0.0f,  -1.0f,  0.0f,
        0.0f,  -1.0f,  0.0f,
        0.0f,  -1.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
        0.0f,  1.0f,  0.0f,
   };

   public Object3D() {
      ByteBuffer byteBuffer = ByteBuffer.allocateDirect(vertices.length * 4);
      byteBuffer.order(ByteOrder.nativeOrder());
      vertexBuffer = byteBuffer.asFloatBuffer();
      vertexBuffer.put(vertices);
      vertexBuffer.position(0);

      byteBuffer = ByteBuffer.allocateDirect(normals.length * 4);
      byteBuffer.order(ByteOrder.nativeOrder());
      normalsBuffer = byteBuffer.asFloatBuffer();
      normalsBuffer.put(normals);
      normalsBuffer.position(0);
   }

   public void Draw(GL10 gl) {
      gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
      gl.glVertexPointer(3, GL10.GL_FLOAT, 0, vertexBuffer);
      gl.glNormalPointer(GL10.GL_FLOAT, 0, normalsBuffer);
      gl.glDrawArrays(GL10.GL_TRIANGLE_STRIP, 0, vertices.length / 3);
      gl.glDisableClientState(GL10.GL_VERTEX_ARRAY);
      gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);
   }
}

Здесь мы объявили нормали(не очень корректно, но разница на лицо) и подключили их к отрисовке.

На этом я бы хотел закончить эту статью и, в заключение, показать Вам что должно получиться:

af61cdfbf34bb2cb6d5693376acc5244.png

В статье использовались материалы статьи Дениса Кулагина.
OGLPart3.7z ( 2.65 МБ)
1.5882       7139        03.11.2015 11:00:32        7

0  
03.11.2015 17:41:48
Очередная отличная статья :)
0  
03.11.2015 17:59:51
Благодарен :)
0  
03.11.2015 18:11:19
Отправил в личных сообщениях форума пару мыслей по оформлению. Возможно вам не пришло уведомление.
0  
03.11.2015 18:37:41
Пришло, читаю :) Уведомления, как я понял, отлично работают )
0  
04.02.2016 15:56:14
у меня в одном месте полигоны лагают. Если выставить lightPosition={-10.0f,-5.0f,0.0f,1.0f}; , то есть источник света будет слева и немного повыше куба, то будет видно как моргает полигон. Как это исправить? и связано ли это с тем, что карты нормали у вас немного неправильны заданы?
0.0171  
05.02.2016 09:18:42
Думаю, что это связано именно с нормалями и вершинами. Больше даже с вершинами.
Лучше всего рисовать стороны куба припомощи glPushMatrix() и glPopMatrix().

В сети есть примеры, недавно натыкался.
Если появится время, то напишу новый пост, используя именно такой подход.

Благодарю за фидбек.
0  
06.02.2016 14:09:38
было бы неплохо, потому что ваши уроки пока что наиболее понятны)
^