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


Доброго времени суток.
Это четвертая часть цикла статей “Разработка OpenGL приложения под Android”.
Предыдущие статьи:
Разработка OpenGL приложения под Android. Часть 0. Введение.
Разработка OpenGL приложения под Android. Часть 1. Знакомство.
Разработка OpenGL приложения под Android. Часть 2. Матрицы.
Сегодня мы будем работать с освещением.
Это очень важная тема, умение работать с основами освещения пригодится Вам в большинстве Ваших собственных проектов.
Здесь может возникнуть вопрос зачем что-то менять, если по умолчанию в OpenGL уже выставлен источник света и мы и без того видим наши объекты.
Дело в том, что выставленный по умолчанию свет нельзя редактировать: изменять его цвет, насыщенность, положение и другие параметры. Но самое главное это то, что он всего один. Если Вам, к примеру, понадобится еще свет от ламп или фонарика, то стандартное освещение уже не поможет.
Другими словами, стандартное освещение отлично подходит для базовой настройки приложения и никуда не годится в реальных условиях.
- Теперь немного теории.
В OpenGL версии 1.0, которую мы сейчас используем, на сцене могут присутствовать до восьми источников света.
Включение и отключение их производится при помощи команд glEnable() и glDisable() которые принимают в качестве параметра описанный источник(GL_LIGHTi, где i - число от 0 до 7).
Допустим, мы включили источник света, но этого все еще не достаточно. Теперь нам необходимо задать его свойства. В OpenGL это делается при помощи команды glLight().
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 позволяют задавать такой закон в виде обратно-квадратичной функции от расстояния:

Изображение размещалось на стороннем сервисе и сейчас недоступно.
Т.е. для точечного источника света, кроме свойств излучения, можно задать ещё четыре параметра:
Параметр 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) |
Как видно из таблицы, по умолчанию, интенсивность света не убывает с расстоянием.
Позиция источника света (в случае направленного источника - направление) задается в текущей модельной системе координат.
Например, после выполнения кода:
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) во внешней (мировой) системе координат.
- Прожекторы
Одной из разновидностей точечного источника является прожектор. Для него применимы все параметры, что и для точечного источника, но кроме того прожектор позволяет ограничить распространение света конусом.
Для этого конуса можно задать коэффициент убывания интенсивности, в зависимости от угла между осью конуса и лучом распространения света.

Изображение размещалось на стороннем сервисе и сейчас недоступно.
Параметры, специфические для прожектора:
Параметр pname команды glLightfv
| Значение по умолчанию | Краткий комментарий |
|---|---|
| (0.0, 0.0, -1.0) | (x, y, z) - направление прожектора (ось ограничивающего конуса) |
| 180.0 | угол между осью и стороной конуса (он же половина угла при вершине) |
| 0.0 | экспонента убывания интенсивности |
С теорией мы на этом закончим и начнем писать код.
Для начала нам понадобится проект из прошлого урока.
Мы не будем создавать новый, это нудно и не интересно создавать все сначала.
Мы просто модернизируем старое.
И так, что у нас есть?
А есть у нас три самописных класса: OpenGLRenderer, Object3D и Background.
На данном этапе нужно определить в каком из них нужно прописывать освещение.
Background отпадает сразу. В данный момент он не должен меняться от освещения.
Object3D тоже отпадает. Хотя именно для его отображения мы работаем с освещением, но прописывать свет в этом классе пока нет смысла.
Решено. Остается OpenGLRenderer. Он управляет нашими классами, будет управлять и светом.
Открываем его и редактируем функцию onSurfaceChanged() до следующего вида:
@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():
@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 следующим образом:
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,
};
Вот теперь можно запустить проект и полюбоваться.

Получилось и хорошо и плохо одновременно.
Хорошо то, что освещение работает, но плохо то, что работает не корректно.
Мы вращаем матрицу и источник света вращается вместе с объектом, но это нам подходит сейчас. Мы сможем рассмотреть как освещается объект со всех сторон.
Самое плохое это то, что мы не видим граней, но это легко исправить с помощью нормалей.
Приведем наш класс Object3D к следующему виду:
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);
}
}
Здесь мы объявили нормали(не очень корректно, но разница на лицо) и подключили их к отрисовке.
На этом я бы хотел закончить эту статью и, в заключение, показать Вам что должно получиться:

В статье использовались материалы статьи Дениса Кулагина.