Свет в opengl. Уроки и примеры программирования Array ()

Чтож, господа. За последнее время мы довольно много узнали об OpenGL, в том числе научились управлять камерой , работать с текстурами , а также с моделями . Настало время поговорить о чем-то намного более интересном, а именно — об освещении. Интересна эта тема, потому что ничего готового для работы со светом в OpenGL нет, все нужно писать самостоятельно на шейдерах. В рамках этой заметки мы рассмотрим освещение по Фонгу. Это довольно большая тема, поэтому говорить мы будем исключительно об освещении . В том, как делаются тени , придется разобраться в другой раз.

Сохранение и использование нормалей

Прежде, чем перейти непосредственно к освещению, нам понадобится такая штука, как нормали.

Мы уже знаем, что у моделей есть вершины и соответствующие этим вершинам UV-координаты. Для создания освещения нам понадобится еще кое-какая информация о моделях, а именно — нормали. Нормаль — это единичный вектор, соответствующей вершине (или, как вариант, полигону, но это не наш случай). Какую именно роль играют нормали при реализации освещения мы узнаем ниже. Пока достаточно сказать, что нормали действительно очень важны. Например, благодаря им поверхности выглядят более гладкими и можно отличить шар от правильного выпуклого многогранника вроде икосаэдра . А раз нормали так важны, нам нужно научиться их сохранять при преобразовании моделей Blender в наш собственный формат.

Соответствующие изменения довольно тривиальны. Мы получаем нормали точно так же, как получали координаты вершин и UV-координаты:

// часть тела процедуры importedModelCreate

for (unsigned int j = 0 ; j < face.mNumIndices ; ++ j) {
unsigned int index = face.mIndices [ j] ;
aiVector3D pos = mesh- > mVertices[ index] ;
aiVector3D uv = mesh- > mTextureCoords[ 0 ] [ index] ;
aiVector3D normal = mesh- > mNormals[ index] ;

VerticesBuffer[ verticesBufferIndex++ ] = pos.x ;
verticesBuffer[ verticesBufferIndex++ ] = pos.y ;
verticesBuffer[ verticesBufferIndex++ ] = pos.z ;
verticesBuffer[ verticesBufferIndex++ ] = normal.x ;
verticesBuffer[ verticesBufferIndex++ ] = normal.y ;
verticesBuffer[ verticesBufferIndex++ ] = normal.z ;
verticesBuffer[ verticesBufferIndex++ ] = uv.x ;
verticesBuffer[ verticesBufferIndex++ ] = 1.0f - uv.y ;
}

Аналогично изменяется процедура оптимизации модели. А в процедуре modelLoad вместо двух массивов атрибутов нам теперь потребуется три:

// часть тела процедуры modelLoad

GlBindVertexArray(modelVAO) ;
glEnableVertexAttribArray(0 ) ;
glEnableVertexAttribArray(1 ) ;
glEnableVertexAttribArray(2 ) ;

GlBindBuffer(GL_ARRAY_BUFFER, modelVBO) ;
glBufferData(GL_ARRAY_BUFFER, header- > verticesDataSize, verticesPtr,
GL_STATIC_DRAW) ;

GLsizei stride = 8 * sizeof (GLfloat) ;
glVertexAttribPointer(0 , 3 , GL_FLOAT, GL_FALSE, stride, nullptr) ;
glVertexAttribPointer(1 , 3 , GL_FLOAT, GL_FALSE, stride,
(const void * ) (3 * sizeof (GLfloat) ) ) ;
glVertexAttribPointer(2 , 2 , GL_FLOAT, GL_FALSE, stride,
(const void * ) (6 * sizeof (GLfloat) ) ) ;

Также нам дополнительно понадобится uniform-переменная с матрицей M:

GLint uniformM = getUniformLocation(programId, "M" ) ;

// ...

GlUniformMatrix4fv(uniformM, 1 , GL_FALSE, & towerM[ 0 ] [ 0 ] ) ;

… чтобы в vertex shader правильно повернуть нормаль в пространстве:

#version 330 core

Layout(location = 0 ) in vec3 vertexPos;
layout(location = 1 ) in vec3 vertexNorm;
layout(location = 2 ) in vec2 vertexUV;

uniform mat4 MVP;
uniform mat4 M;

out vec2 fragmentUV;
out vec3 fragmentNormal;
out vec3 fragmentPos;

void main() {
fragmentUV = vertexUV;
fragmentNormal = (M * vec4 (vertexNorm, 0 ) ) . xyz ;
fragmentPos = (M * vec4 (vertexPos, 1 ) ) . xyz ;

gl_Position = MVP * vec4 (vertexPos, 1 ) ;
}

Наконец, fragment shader принимает интерполированную по трем вершинам нормаль:

// ...

void main() {

// ...
}

Таким вот незамысловатым образом мы получаем нормали для фрагментов .

Что такое освещение по Фонгу

Как было отмечено, освещение в OpenGL пишется на шейдерах самим программистом. Понятно, что есть больше одного способа реализовать это освещение, каждый со своей степенью реалистичности и требовательностью к ресурсам. А у каждого способа еще может быть бесчисленное количество конкретных реализаций. Насколько я понимаю, эффективное и реалистичное освещение в реальном времени все еще является областью активных исследований. В рамках этой заметки мы рассмотрим освещение по Фонгу, которое одновременно является довольно реалистичным и простым в реализации.

Важно понимать разницу между следующими понятиями:

  • Затенение по Гуро (Gouraud shading) — это когда вы вычисляете освещенность каждой вершины, а освещенность фрагментов между ними интерполируется;
  • Затенение по Фонгу (Phong shading) — когда освещенность вычисляется отдельно для каждого фрагмента;
  • Освещение по Фонгу (Phong lighting или Phong reflection model) — конкретный способ освещения, о котором идет речь в этой заметке и который можно использовать как в затенении по Гуро, так и в затенении по Фонгу;

Не удивительно, что Phong shading и Phong lighting часто путают, и в некоторых туториалах можно прочитать ерунду вроде «Идея освещения Фонга (Phong shading) заключается в использовании трех компонентов …» что сразу заставляет сильно усомниться в авторитете написавшего этот туториал человека.

Насколько я смог понять, в современных приложениях затенение по Гуро почти не используется, вместо него предпочтение отдается затенению по Фонгу. В рамках данного поста мы тоже будем использовать затенение по Фонгу, то есть, освещение будет вычислять отдельно для каждого фрагмента. Конкретный способ освещения, которым мы воспользуемся — освещение по Фонгу. Этот способ заключается в следующем.

По разным формулам вычисляется три компонента освещения:

  • Фоновое освещение (ambient lighting) — имитация света, достигшего заданной точки после отражения от других объектов. При расчете фонового освещения не учитываются ни нормали, ни текущее положение камеры;
  • Рассеянное освещение (diffuse lighting) — свет от источника, рассеянный после попадания в заданную точку. В зависимости от угла, под которым падает свет, освещение становится сильнее или слабее. Здесь учитываются нормали, но не положение камеры;
  • Отраженное освещение (specular lighting) — свет от источника, отраженный после попадания в заданную точку. Отраженный свет виден, если он попадает в камеру. Поэтому здесь учитываются как нормали, так и положение камеры;

Затем результаты суммируются, в результате чего получается общее освещение.

Чтобы стало еще интереснее, источники света бывают разные. Очевидно, что солнце на улице и фонарик в темноте освещают сцену совсем по-разному. Для начала мы рассмотрим наиболее простой источник — направленный свет.

Направленный свет (directional light)

Направленный свет — это имитация бесконечно удаленного источника света. Возьмем, например, Солнце. Солнце находится очень далеко от Земли. Поэтому у поверхности Земли можно с большой точностью считать все лучи света от Солнца параллельным. Направленный свет характеризует его направление, цвет, а также кое-какие коэффициенты, которые понадобятся нам ниже:

struct DirectionalLight {
vec3 direction;

vec3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity;
} ;

В коде fragment shader определим процедуру calcDirectionalLight, которая будет использоваться как-то так:

in vec3 fragmentPos;
uniform vec3 cameraPos;
uniform DirectionalLight directionalLight;

// ...

void main() {
// normal should be corrected after interpolation
vec3 normal = normalize (fragmentNormal) ;


directionalLight) ;

// ...
}

Рассмотрим реализацию процедуры.

vec4 calcDirectionalLight(vec3 normal, vec3 fragmentToCamera,
DirectionalLight light) {
vec4 ambientColor = vec4 (light. color , 1 ) * light. ambientIntensity ;

// ...
}

Сначала вычисляется первый компонент — фоновое освещение. Это просто цвет излучаемого света умноженный на интенсивность фонового освещения. Пока все просто.

// ...

float diffuseFactor = max (0.0 , dot (normal, - light. direction ) ) ;
vec4 diffuseColor = vec4 (light. color , 1 ) * light. diffuseIntensity
* diffuseFactor;

// ...

Рассеянное освещение. Переменная diffuseFactor представляет собой косинус угла между нормалью к фрагменту и вектором, направленным от фрагмента к источнику света. Если свет падает перпендикулярно поверхности, угол равен нулю. Косинус этого угла равен единице и освещенность максимальна (см статью на Wikipedia о Законе Ламберта). С увеличением угла косинус уменьшается и становится равным нулю, если свет идет параллельно поверхности. Если косинус отрицательный, значит источник света находится где-то за поверхностью и она не освещена, поэтому отрицательные значения мы обращаем в ноль при помощи max(0.0, ...) . Помимо угла, под которым падает свет, также учитывается интенсивность рассеянного освещения diffuseIntensity.

// ...
vec3 lightReflect = normalize (reflect (light. direction , normal) ) ;
float specularFactor = pow (
max (0.0 , dot (fragmentToCamera, lightReflect) ) ,
materialSpecularFactor
) ;
vec4 specularColor = light. specularIntensity * vec4 (light. color , 1 )
* materialSpecularIntensity * specularFactor;
// ...

Отраженное освещение. Переменная lightReflect — это единичный вектор, задающий направление отраженного света. Переменная specularFactor вычисляется похожим на diffuseFactor способом, только на этот раз учитывается косинус угла между направлением, в котором отразился свет, и направлением от фрагмента до камеры. Если этот угол равен нулю, значит отраженный свет летит прямо в камеру и блики на поверхности максимальны. Если угол велик, значит никаких бликов не должно быть видно. Здесь materialSpecularFactor является uniform переменной. Чем она больше, тем меньше по площади блики на поверхности объекта. Также используется переменная materialSpecularIntensity, определяющая яркость бликов. Заметьте, что все это — свойства материала, а не света. Например, метал отражает свет, и потому имеет блики. А дерево свет не отражает, и потом вы никогда не видите бликов на деревьях (конечно, если поверхность сухая, и так далее).

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

Наконец, три компонента складываются и возвращается результат:

// ...

return ambientColor + diffuseColor + specularColor;
}

Не так уж и сложно, правда?

Точечный источник света (point light)

Точечный источник света — это, к примеру, горящая лампочка. Свет от лампочки направлен во все стороны. Поэтому точечный источник света не характеризуется направлением света, но характеризуется положением источника в пространстве:

struct PointLight {
vec3 position;

vec3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity; // for debug purposes, should be set to 1.0
} ;

Освещенность от точечного источника света легко вычисляется через уже имеющуюся процедуру calcDirectionalLight:

vec4 calcPointLight(vec3 normal, vec3 fragmentToCamera,
PointLight light) {
vec3 lightDirection = normalize (fragmentPos - light. position ) ;
float distance = length (fragmentPos - light. position ) ;
float pointFactor = 1.0 / (1.0 + pow (distance , 2 ) ) ;

DirectionalLight tempDirectionalLight = DirectionalLight(
lightDirection,
light. color ,
light. ambientIntensity ,
light. diffuseIntensity ,
light. specularIntensity
) ;
return pointFactor * calcDirectionalLight(normal, fragmentToCamera,
tempDirectionalLight) ;
}

Имея координаты фрагмента и источника света, можно легко вычислить направление света к заданному фрагменту через разность векторов. Множитель pointFactor отражает факт затухания света с квадратом расстояния до его источника (в соответствии с формулой зависимости площади поверхности сферы от радиуса). При вычислении pointFactor в делителе дополнительно прибавляется единица, чтобы предотвратить возможность деления на ноль. После этого все вычисляется точно так же, как для направленного света.

Прожектор (spot light)

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

struct SpotLight {
vec3 direction;
vec3 position;
float cutoff;

vec3 color;
float ambientIntensity;
float diffuseIntensity;
float specularIntensity; // for debug purposes, should be set to 1.0
} ;

Соответствующая процедура:

vec4 calcSpotLight(vec3 normal, vec3 fragmentToCamera,
SpotLight light) {
vec3 spotLightDirection = normalize (fragmentPos - light. position ) ;
float spotAngleCos = dot (spotLightDirection, light. direction ) ;
float attenuation = (1.0 - 1.0 * (1.0 - spotAngleCos) /
(1.0 - light. cutoff ) ) ;
float spotFactor = float (spotAngleCos > light. cutoff ) * attenuation;

PointLight tempPointLight = PointLight(
light. position ,
light. color ,
light. ambientIntensity ,
light. diffuseIntensity ,
light. ambientIntensity
) ;
return spotFactor * calcPointLight(normal, fragmentToCamera,
tempPointLight) ;
}

Направление света вычисляется точно так же, как и для точечного источника. Затем вычисляется косинус угла между этим направлением и направлением, указанным в свойствах самого источника света. При помощи выражения float(spotAngleCos > light.cutoff) свет жестко обрезается до указанного угла. Множитель attenuation добавляет плавное затухание света по мере отдаления фрагментов от направления света, указанного в свойствах источника. После этого все вычисления сводятся к вычислениям для точечного источника света.

Гамма-коррекция

Целиком процедура main во fragment shader выглядит так:

void main() {
// normal should be corrected after interpolation
vec3 normal = normalize (fragmentNormal) ;
vec3 fragmentToCamera = normalize (cameraPos - fragmentPos) ;

vec4 directColor = calcDirectionalLight(normal, fragmentToCamera,
directionalLight) ;
vec4 pointColor = calcPointLight(normal, fragmentToCamera,
pointLight) ;
vec4 spotColor = calcSpotLight(normal, fragmentToCamera, spotLight) ;
vec4 linearColor = texture(textureSampler, fragmentUV) *
(vec4 (materialEmission, 1 ) + directColor +
pointColor + spotColor) ;

vec4 gamma = vec4 (vec3 (1.0 / 2.2 ) , 1 ) ;
color = pow (linearColor, gamma) ; // gamma-corrected color
}

На materialEmission не обращайте особого внимания. Это просто еще одно свойство материала, добавляющее ему самостоятельное свечение. Многие объекты светятся сами по себе. Взять те же лампочки, которые служат источником света для других объектов. Мы ведь должны видеть их в полной темноте, даже если лампочки не освещены никаким другим источником света, верно?

Что действительно заслуживает внимания — это гамма-коррекция , которая заключается в возведении всех компонентов света в степень 1/2.2. До сих пор мы работали в линейном пространстве цветов, исходя из предположения, что цвет с яркостью 1.0 в два раза ярче цвета с яркостью 0.5. Проблема в том, что человеческий глаз воспринимает яркость не линейно. Поэтому для получения реалистичного освещения необходимо после всех вычислений в линейном пространстве производить гамма-коррекцию.

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

Достаточно заменить в коде загрузки текстур все константы:

GL_COMPRESSED_RGBA_S3TC_DXT1_EXT
GL_COMPRESSED_RGBA_S3TC_DXT3_EXT
GL_COMPRESSED_RGBA_S3TC_DXT5_EXT

GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT
GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT
GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT

… соответственно. Так мы сообщим, что к изображением была применена гамма-коррекция, которую нужно отменить. Об остальном OpenGL позаботится сам.

В реальных приложениях параметр gamma (у нас gamma = 2.2) лучше выносить в настройки программы, чтобы пользователь при желании мог немного подстроить его под свой монитор.

Заключение

Настало время разглядывать картинки!

Здесь мы видим различные компоненты освещения. Слева направо, сверху вниз: фоновое, рассеянное, отраженное, все три вместе. Как видите, на сцену была добавлена модель тора . Из-за сложного расположения нормалей эту модель рекомендуется использовать для тестирования освещения.

Различные источники света. Слева направо, сверху вниз: белый направленный свет, красный точечный источник света, синий прожектор, все три вместе.

Отмечу еще раз, что один и тот же метод освещения может иметь разные реализации. Например, можно сделать свойства материала ambient, diffuse и specular color, что позволит рисовать красные объекты, рассеивающие зеленый цвет и имеющие синие блики. В некоторых реализациях освещения по Фонгу я видел вычисление фонового освещения один раз, а не для каждого источника света. Также я видел реализации, где свет от точечного источника затухал не просто пропорционально квадрату расстояния до него (d * d), а по более общей формуле (в стиле A + B*d + C*d*d). Кто-то делает ambient intensity и diffuse intensity свойством не только источника света, но и материала. Не уверен, правда, насколько все это имеет отношение к реалистичности освещения. Но в качестве домашнего задания можете поиграться со всем этим.

Освещение какого-либо пространства - это процесс, благодаря которому это пространство наполняется светом и все находящиеся в нём предметы делаются видимыми.
Освещение любого объекта зависит от двух факторов:

  • Первый - это материал, из которого сделан объект.
  • Второй - это свет, которым он освещен.

В зависимости от реализации OpenGL на сцене могут присутствовать восемь и более источников света.По умолчанию освещение отключено. Включить нулевой источник света можно командой:

  • glEnable(GL_LIGHT0);

Остальные включаются аналогичным способом, где вместо GL_LIGHT0 указывается GL_LIGHTi. После того, как источник включен, необходимо задать его параметры. Если монотонное тело у вас равномерно освещено, то вы не можете увидеть его рельеф. Поэтому нам нужно использовать источники света.
В OpenGL существует три типа источников света:

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

Для управления свойствами источника света используются команды glLight*:

  • glLightf(GLenum light, GLenum pname, GLfloat param);
    glLightfv(GLenum light, GLenum pname, const GLfloat *param);

Параметр light указывает OpenGL для какого источника света задаются параметры. Команда glLightf используется для задания скалярных параметров, а glLightfv используется для задания векторных характеристик источников света.

Сначала рассмотрим функцию, которая устанавливает базовые настройки. Когда вы разрешили освещение, то вы можете уже устанавливать фоновую освещенность. По умолчанию, значение фоновой освещенности равно (0.2, 0.2, 0.2, 1). Создайте новый проект, скопируйте туда шаблонный файл и отключите освещение. Вы с трудом сможете различить сферу на экране. С помощью функции glLightModel вы можете установить фоновое освещение. Если вы повысите его до (1,1,1,1), т.е. до максимума, то включать источники света вам не понадобится. Вы их действия просто не заметите, т.к. объект уже максимально освещен. И получится, что вы как бы отключили освещение. В общем, добавьте в main вызов следующей функции:

  • float ambient = {0.5, 0.5, 0.5, 1};
    ...
    glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient);

Попробуйте изменить параметры и посмотрите на результат.

Материал
Материал может рассеивать, отражать и излучать свет. Свойства материала устанавливаются при помощи функции

  • glMaterialfv(GLenum face, GLenum pname, GLtype* params)

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

  • GL_BACK задняя грань
    GL_FONT передняя грань
    GL_FRONT_AND_BACK обе грани

Второй параметр функции glMaterialfv определяет свойство материала, которое будет установлено, и может принимать следующие значения.

  • GL_AMBIENT рассеянный свет
    GL_DIFFUSE тоже рассеянный свет
    GL_SPECULAR отраженный свет
    GL_EMISSION излучаемый свет
    GL_SHININESS степень отраженного света
    GL_AMBIENT_AND_DIFFUSE оба рассеянных света

Цвет задается в виде массива из четырех элементов - RGBA. В случае GL_SHININESS params указывает на число типа float, которое должно быть в диапазоне от 0 до 128.
Вам надо всего лишь модифицировать функцию display.

  • void CALLBACK display(void)
    {
    GLUquadricObj *quadObj;
    GLfloat front_color = {0,1,0,1};
    GLfloat back_color = {0,0,1,1};
    quadObj = gluNewQuadric();
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glMaterialfv(GL_FRONT, GL_DIFFUSE, front_color);
    glMaterialfv(GL_BACK, GL_DIFFUSE, back_color);
    glPushMatrix();
    glRotated(110, -1,1,0);
    gluCylinder(quadObj, 1, 0.5, 2, 10, 10);
    glPopMatrix();
    gluDeleteQuadric(quadObj);
    auxSwapBuffers();
    }

И вы должны разрешить режим освещенности для двух граней. По умолчанию он запрещен. Добавьте в функцию main следующую строчку.

  • glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);

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

  • GL_POSITION (0.0, 0.0, 1.0, 0.0) //(x, y, z, w) направление источника направленного света

Первые три компоненты (x, y, z) задают вектор направления, а компонента w всегда равна нулю (иначе источник превратится в точечный).

Функции затухания
Это функция изменения интенсивности освещения(интенсивность света не убывает с расстоянием) , используется вместе с точечным освещением

  • GL_POSITION(0.0, 0.0, 1.0, 0.0)//позиция источника света (по умолчанию источник света направленный)
  • GL_CONSTANT_ATTENUATION 1.0 //постоянная k_const в функции затухания f(d)
  • GL_LINEAR_ATTENUATION 0.0 //коэффициент k_linear при линейном члене в функции затухания f(d)
  • GL_QUADRATIC_ATTENUATION 0.0 //коэффициент k_quadratic при квадрате расстояния в функции затухания f(d)

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

  • GL_SPOT_DIRECTION (0.0, 0.0, -1.0) //(x, y, z) - направление прожектора (ось ограничивающего конуса)
  • GL_SPOT_CUTOFF 180.0 //угол между осью и стороной конуса (он же половина угла при вершине)
  • GL_SPOT_EXPONENT 0.0 //экспонента убывания интенсивности

Тени
Тени напрямую не поддерживаются библиотекой OpenGL, поэтому их нужно разбирать отдельно.

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

Свойства материала

Для задания параметров текущего материала используются команды

void glMaterial (GLenum face, GLenum pname, GLtype param)
void glMaterialv (GLenum face, GLenum pname, GLtype *params)

С их помощью можно определить рассеянный, диффузный и зеркальный цвета материала, а также цвет степень зеркального отражения и интенсивность излучения света, если объект должен светиться. Какой именно параметр будет определяться значением param, зависит от значения pname:

GL_AMBIENT параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют рассеянный цвет материала (цвет материала в тени).

Значение по умолчанию: (0.2, 0.2, 0.2, 1.0).

GL_DIFFUSE параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет диффузного отражения материала.

Значение по умолчанию:(0.8, 0.8, 0.8, 1.0).

GL_SPECULAR параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет зеркального отражения материала.

GL_SHININESS параметр params должен содержать одно целое или вещественное значение в диапазоне от 0 до 128, которое определяет степень зеркального отражения материала.

Значение по умолчанию: 0.

GL_EMISSION параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют интенсивность излучаемого света материала.

Значение по умолчанию: (0.0, 0.0, 0.0, 1.0).

GL_AMBIENT_AND_DIFFUSE эквивалентно двум вызовам команды glMaterial…() со значением pname GL_AMBIENT и GL_DIFFUSE и одинаковыми значениями params.

Из этого следует, что вызов команды glMaterial() возможен только для установки степени зеркального отражения материала. В большинстве моделей учитывается диффузный и зеркальный отраженный свет; первый определяет естественный цвет объекта, а второй - размер и форму бликов на его поверхности.

Параметр face определяет тип граней, для которых задается этот материал и может принимать значения GL_FRONT, GL_BACK или GL_FRONT_AND_BACK.

Если в сцене материалы объектов различаются лишь одним параметром, рекомендуется сначала установить нужный режим, вызвав glEnable() c параметром GL_COLOR_MATERIAL, а затем использовать команду

void glColorMaterial (GLenum face, GLenum pname)

где параметр face имеет аналогичный смысл, а параметр pname может принимать все перечисленные значения. После этого, значения выбранного с помощью pname свойства материала для конкретного объекта (или вершины) устанавливается вызовом команды glColor…(), что позволяет избежать вызовов более ресурсоемкой команды glMaterial…() и повышает эффективность программы.

Добавить в сцену источник света можно с помощью команд

void glLight (GLenum light, GLenum pname, GLfloat param)
void glLight (GLenum light, GLenum pname, GLfloat *params)

Параметр light однозначно определяет источник,и выбирается из набора специальных символических имен вида GL_LIGHTi, где i должно лежать в диапазоне от 0 до GL_MAX_LIGHT, которое не превосходит восьми.

Оставшиеся два параметра имеют аналогичный смысл, что и в команде glMaterial…(). Рассмотрим их назначение (вначале описываются параметры для первой команды, затем для второй):

GL_SPOT_EXPONENT параметр param должен содержать целое или вещественное число от 0 до 128, задающее распределение интенсивности света. Этот параметр описывает уровень сфокусированности источника света.

Значение по умолчанию: 0 (рассеянный свет).

GL_SPOT_CUTOFF параметр param должен содержать целое или вещественное число между 0 и 90 или равное 180, которое определяет максимальный угол разброса света. Значение этого параметра есть половина угла в вершине конусовидного светового потока, создаваемого источником.

Значение по умолчанию: 180 (рассеянный свет).

GL_AMBIENT параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет фонового освещения.

Значение по умолчанию: (0.0, 0.0, 0.0, 1.0).

GL_DIFFUSE параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет диффузного освещения.

GL_SPECULAR параметр params должен содержать четыре целых или вещественных значения цветов RGBA, которые определяют цвет зеркального отражения.

Значение по умолчанию: (1.0, 1.0, 1.0, 1.0)для LIGHT0 и (0.0, 0.0, 0.0, 1.0) для остальных.

GL_POSITION параметр params должен содержать четыре целых или вещественных, которые определяют положение источника света. Если значение компоненты w равно 0.0, то источник считается бесконечно удаленным и при расчете освещенности учитывается только направление на точку (x,y,z), в противном случае считается, что источник расположен в точке (x,y,z,w).

Значение по умолчанию: (0.0, 0.0, 1.0, 0.0).

GL_SPOT_DIRECTION параметр params должен содержать четыре целых или вещественных числа, которые определяют направление света.

Значение по умолчанию: (0.0, 0.0, -1.0, 1.0).

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

Для использования освещения сначала надо установить соответствующий режим вызовом команды glEnable (GL_LIGHTNING), а затем включить нужный источник командой glEnable(GL_LIGHTn).

Модель освещения

В OpenGL используется модель освещения Фонга, в соответствии с которой цвет точки определяется несколькими факторами: свойствами материала и текстуры, величиной нормали в этой точке, а также положением источника света и наблюдателя. Для корректного расчета освещенности в точке надо использовать единичные нормали, однако команды типа glScale…(), могут изменять длину нормалей. Чтобы это учитывать, используется уже упоминавшийся режим нормализации нормалей, который включается вызовом команды glEnable(GL_NORMALIZE).

Для задания глобальных параметров освещения используются команды

void glLightModel (GLenum pname, GLenum param)
void glLightModelv (GLenum pname, const GLtype *params)

Аргумент pname определяет, какой параметр модели освещения будет настраиваться и может принимать следующие значения:

GL_LIGHT_MODEL_LOCAL_VIEWER параметр param должен быть булевским и задает положение наблюдателя. Если он равен FALSE, то направление обзора считается параллельным оси -z, вне зависимости от положения в видовыx координатах. Если же он равен TRUE, то наблюдатель находится в начале видовой системы координат. Это может улучшить качество освещения, но усложняет его расчет.

Значение по умолчанию: FALSE.

GL_LIGHT_MODEL_TWO_SIDE параметр param должен быть булевским и управляет режимом расчета освещенности как для лицевых, так и для обратных граней. Если он равен FALSE, то освещенность рассчитывается только для лицевых граней. Если же он равен TRUE, расчет проводится и для обратных граней. Значение по умолчанию: FALSE.

GL_LIGHT_MODEL_AMBIENT параметр params должен содержать четыре целых или вещественных числа, которые определяют цвет фонового освещения даже в случае отсутствия определенных источников света.

Значение по умолчанию:(0.2, 0.2, 0.2,1.0).

Освещение в OpenGL ES – это полезная особенность, которая может придать 3D -играм приятный оттенок. Чтобы использовать подобную функциональность, сначала нам необходимо понять модель освещения OpenGL ES.

Как работает освещение

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

Интенсивность, с которой свет отражается объектом, зависит от множества факторов. Самый главный фактор, на который мы обращаем внимание, – это угол, с которым световой луч падает на поверхность. Чем ближе этот угол к прямому, тем больше интенсивность, с которой свет отразится от объекта. Это проиллюстрировано на рис. 11.1.

Как только световой луч упадет на поверхность, он отразится в двух различных направлениях. Большая часть света отразится рассеянно. Это означает, что отраженные световые лучи неравномерно рассыпаны в случайном порядке по поверхности объекта. Некоторые лучи отражаются зеркально. Это означает, что световые лучи отразятся назад так, будто они падают на идеальное зеркало. На рис. 11.2 показана разница между рассеянным и зеркальным отражениями.

Рис. 11.1. Чем ближе угол к прямому, тем больше интенсивность отраженного света

Рис. 11.2. Рассеянное и зеркальное отражения

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

Когда свет падает на поверхность, его отражение также меняет свой цвет в зависимости от химического состава освещаемого объекта. Объекты, которые кажутся нам красными, отражают только красные порции света и поглощают все другие частоты. Черный объект – это такой объект, который поглощает практически весь свет, который падает на него.

OpenGL ES позволяет имитировать реальное поведение, определяя источники света и материалы объектов.

Источники освещения

Нас окружает множество разнообразных источников освещения. Солнце постоянно посылает свои фотоны. Мониторы излучают свет, окружающий нас приятным свечением по ночам. Лампочки и фары помогают нам избегать столкновений с различными предметами в темноте. OpenGL ES позволяет создавать четыре типа источников света.

Подсветка. Является сама по себе не источником света, а результатом появления фотонов от других световых источников. Вместе эти случайные фотоны создают некоторый постоянный уровень освещения, не имеющий направления и освещающий все объекты одинаково.

Точечные источники света. Имеют позицию в пространстве и испускают свет во всех направлениях. Например, точечным источником света является лампочка.

Направленные источники освещения. Выражаются как направления в OpenGL ES. Предполагается, что они находятся бесконечно далеко. В идеале Солнце может являться таким источником. Мы можем предположить, что все световые лучи, исходящие от Солнца, попадают на Землю под одинаковым углом из-за расстояния между Землей и Солнцем.

О Светильники. Эти источники похожи на точечные источники освещения тем, что имеют заданную позицию в пространстве. Кроме того, у них есть направление, в котором они излучают световые лучи. Они создают световой конус, ограниченный некоторым радиусом. Примером такого источника света является уличный фонарь.

Мы будем рассматривать только подсветку, а также точечные и направленные источники света. Светильники часто сложно использовать на ограниченных GPU Android-устройств из-за способа расчета освещения в OpenGL ES. Скоро вы поймете, почему это так.

Помимо позиции и направления источника света OpenGL ES позволяет определять цвет или интенсивность света. Эти характеристики выражаются с помощью цвета RGBA. Однако OpenGL ES требует определять четыре различных цвета для одного источника вместо одного.

Подсветка – интенсивность/цвет, вносящий вклад в создание затенения объекта. Объект будет освещен одинаково со всех сторон, независимо от его позиции или ориентации относительно источника света.

Рассеянный – интенсивность/цвет света, которым будет освещен объект после расчета рассеянного отражения. Грани объекта, которые не смотрят на источник света, не будут освещены, как и в реальной жизни.

Зеркальный – интенсивность/цвет, похожий на рассеянный цвет. Однако он влияет только на те точки объекта, которые имеют определенную ориентацию по отношению к источнику света и сенсору.

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

Чаще всего будем применять рассеянные и зеркальные интенсивности источника света, а двум другим укажем значения по умолчанию. Кроме того, большую часть времени будем использовать одинаковый цвет RGBA как для рассеянной, так и для зеркальной интенсивности.

Материалы

Все объекты в нашем мире состоят из какого-либо материала. Каждый материал определяет, как свет, падающий на объект, будет отражаться и изменять цвет отраженного света. OpenGL ES позволяет определять те же четыре цвета RGBA для материала, что и для источника света.

Подсветка – цвет, который объединяется с фоновым цветом любого источника света на сцене.

Рассеянный – цвет, который объединяется с рассеянным цветом любого источника света.

Зеркальный – цвет, который объединяется с зеркальным цветом любого источника света. Он используется для создания бликов на поверхности объекта.

Эмиссивный – продолжаем игнорировать этот тип цвета, поскольку он практически не применяется в нашем контексте.

Рисунок 11.3 иллюстрирует первые три типа свойств материала/источника света: подсветка, рассеянный и зеркальный.

Рис. 11.3. Различные типы материалов/источников света: только подсветка (слева), только рассеянный (посередине), подсветка и рассеянный цвет с зеркальными бликами (справа)

На рис. 11.3 показано влияние различных свойств материалов и источников света на цвет. Подсветка освещает размер равномерно. Рассеянный свет отразится в зависимости от угла, под которым на объект падают световые лучи; площади, которые непосредственно повернуты к источнику света, будут освещены ярче, площади, до которых свет не может добраться, будут темными. На правом изображении вы можете увидеть комбинацию подсветки, рассеянного и зеркального света. Зеркальный свет проявляет себя как белые блики на сфере.

Как OpenGL ES рассчитывает освещение: нормали вершин

Вы знаете, что интенсивность света, отраженного от объекта, зависит от его угла падения на объект. OpenGL ES использует этот факт для расчета освещения. Он применяет для этого нормали вершин, которые необходимо определять в коде так же, как и координаты текстур или цвета вершин. На рис. 11.4 показана сфера и ее нормали вершин.

Рис. 11.4. Сфера и ее нормали вершин

Нормали – это единичные векторы, указывающие направление, к которому повернута поверхность. В нашем случае поверхность – это треугольник. Вместо определения нормали поверхности мы определяем нормаль вершины. Разница между этими нормалями заключается в том, что нормаль вершины может не указывать в ту же сторону, что и нормаль поверхности. Это четко видно на рис. 11.4, где каждая нормаль вершины является усредненной нормалью всех треугольников, к которым принадлежит вершина. Такое усреднение производится для создания гладкой затененности объекта.

При отрисовке объекта с использованием освещения и нормалей вершин OpenGL ES определит угол между каждой вершиной и источником света. Если он знает этот угол, то может рассчитать цвет вершины, основываясь на свойствах материала. Конечным результатом является цвет каждой вершины, который далее применяется к каждому треугольнику в комбинации с рассчитанными цветами других вершин. Этот использованный цвет будет объединен с любыми текстурными преобразованиями, которые мы применим к объекту.

Это звучит довольно пугающе, но на самом деле не все так плохо. Нам нужно разрешить использование освещения и определить источники освещения, материал отрисовываемого объекта и нормали вершин (в дополнение к параметрам вершин, которые мы обычно определяем, например позицию или координаты текстур). Рассмотрим, как это можно реализовать с помощью OpenGL ES.

На практике

Теперь выполним все действия, необходимые для того, чтобы работать с освещением с помощью OpenGL ES. Создадим несколько небольших вспомогательных классов, которые немного упростят работу с источниками света, и поместим их в пакет com.badlogi с.androi dgames.framework.gl.

Разрешение и запрещение освещения

Как и для прочих состояний OpenGL ES, сначала следует подключить названную функциональность. Это можно сделать следующим образом:

После этого освещение будет применено ко всем отрисовываемым объектам. Чтобы получить результат, необходимо определить источники света и материалы, а также нормали вершин. Как только мы закончим отрисовывать все необходимые объекты, освещение можно отключить:

Определение источников освещения

OpenGL ES предоставляет 4 типа источников освещения: подсветка, точечный, направленный и светильник. Рассмотрим, как определить первые три. Чтобы светильники были эффективными и хорошо выглядели, каждая модель должна состоять из огромного количества треугольников. Для множества теперешних мобильных устройств это невозможно.

OpenGL ES позволяет определять максимум 8 источников освещения одновременно, а также один глобальный источник подсветки. Каждый из 8 источников освещения имеет идентификатор, от GL10.GL LIGHT0 до GL10.GL LIGHT7. Если нужно изменить свойства одного из этих источников освещения, это можно сделать, определив соответствующий ему ID.

Разрешить использование источников освещения можно с помощью следующего синтаксиса:

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

Подсветка – это особый случай, поскольку у нее нет идентификатора. На сцене OpenGL ES может существовать только одна подсветка. Рассмотрим этот источник освещения подробнее.

Подсветка

Подсветка – это особый тип освещения. У него нет позиции или направления, только цвет, который применяется ко всем освещаемым объектам одинаково. OpenGL ES позволяет определять глобальную подсветку следующим образом:

Массив ambi entCol or содержит значения RGBA цвета подсветки, представленные как числа с плавающей точкой в диапазоне от 0 до 1. Метод gl LightModel fv принимает в качестве первого параметра константу, определяющую, что мы хотим установить цвет источника фонового освещения, массив чисел с плавающей точкой, который содержит цвет источника, и смещение для массива чисел с плавающей точкой, из которого метод начнет считывать значения RGBA. Поместим код, решающий эту задачу, в небольшой класс. Его код приведен в листинге 11.2.

Листинг 11.2. Класс AmbientLight.java. простая абстракция глобальной подсветки ODenGL ES

Все, что мы делаем, – сохраняем цвет подсветки в массиве чисел с плавающей точкой и предоставляем два метода: один из них используется для установки цвета, а другой, чтобы указать OpenGL ES, что использовать следует именно этот цвет. По умолчанию применяется серый цвет.

Точечные источники освещения

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

Первый параметр – это идентификатор источника света. В этом случае мы используем четвертый источник. Следующий параметр определяет атрибут, который мы хотим изменить. Третий параметр – это массив чисел с плавающей точкой, содержащий значения RGBA, а последний – это смещение в данном массиве. Определить позицию источника так же просто:

Мы снова определяем атрибут, который хотим изменить (в данном случае позицию), массив из четырех элементов содержит х-, у- и z-координату источника света в создаваемом мире. Обратите внимание, четвертый элемент массива должен быть равен единице, если источник света имеет позицию. Поместим это во вспомогательный класс. Его код содержится в листинге 11.3.

Листинг 11.3. Класс Point.Light.java, простая абстракция точечных источников света OpenGL ES

Наш вспомогательный класс содержит фоновые, рассеянные и зеркальные цветовые компоненты света, а также позицию (четвертый элемент равен единице). В дополнение мы храним последний идентификатор, используемый для данного источника, поэтому становится возможно создать метод disableO, который отключит свет при необходимости. Также у нас есть метод enableO, который принимает экземпляр класса GL10 и идентификатор источника света (например GL10.GL LIGHT6). Он разрешает использование освещения, устанавливает его атрибуты и сохраняет использованный идентификатор. Метод disableO просто запрещает использование освещения, используя член класса 1ast.Ligh.tId, установленный в методе enablе.

Мы используем разумные значения по умолчанию для фонового, рассеянного и зеркального цветов при инициализации массивов-членов класса. Свет будет белым и не будет создавать никаких бликов, поскольку его зеркальная составляющая черная.

Направленные источники света

Направленные источники света практически идентичны точечным. Единственное различие заключается в том, что они имеют направление вместо позиции. Способ выражения направления несколько запутан. Вместо использования вектора, указывающего направление, OpenGL ES ожидает, что мы определим одну точку. Затем направление будет определено с помощью вектора, соединяющего эту точку и начало координат. Следующий сниппет позволяет создать направленный источник света, исходящий с правой стороны мира:

Мы можем преобразовать его в вектор:

Остальные атрибуты, вроде фонового или рассеянного цвета, идентичны аналогичным атрибутам точечного источника света. В листинге 11.4 показан код небольшого вспомогательного класса, использующегося для создания направленных источников света.

Листинг 11.4. Класс Directi onLi ght.java, простая абстракция направленных источников света в OpenGL ES

Этот вспомогательный класс практически идентичен классу PointLight. Единственное различие заключается в том, что в массиве directi on четвертый элемент равен единице. Кроме того, вместо метода setPosition появился метод setDirecti on . Он позволяет определять направление, например так: (-1; 0; 0), в этом случае свет будет исходить с правой стороны. Внутри метода все компоненты вектора меняют свой знак, таким образом мы преобразовываем направление к формату, ожидаемому OpenGL ES.

Определяем материалы

Материал определяется несколькими атрибутами. Как и в случае с любыми другими объектами OpenGL ES, материал – это состояние, которое будет активно до тех пор, пока мы не изменим его снова или пока не потеряется контекст OpenGL ES. Чтобы установить текущие атрибуты материалов, мы можем сделать следующее:

Как и обычно, нам необходимо определить фоновый, рассеянный и зеркальный RGBA-цвета. Это можно сделать так же, как и ранее, – с помощью массивов чисел с плавающей точкой, состоящих из четырех элементов.

Объединить эти действия в один вспомогательный класс очень просто. Результат вы можете увидеть в листинге 11.5.

Листинг 11.5. Класс Material Java, простая абстракция материалов OpenGL ES

Здесь тоже нет ничего удивительного. Мы просто сохраняем три компонента, описывающих материал, а также предоставляем функции для установки их значений и метод enabl е, которые передают их OpenGL ES.

У OpenGL ES есть еще один козырь в рукаве, когда речь идет о материалах. Обычно он вместо метода glMaterialfvO использует нечто, называемое цветом материала. Это означает, что вместо фонового и рассеянного цветов, определяемых методом glMateri al fv, OpenGL ES примет цвет вершин наших моделей в качестве фонового и рассеянного цветов материала. Чтобы разрешить использование этой особенности, необходимо просто вызвать ее:

Обычно я именно так и поступаю, потому что фоновый и рассеянный цвета часто одинаковы. Поскольку я не использую зеркальные блики в большинстве моих игр и демонстраций, я вполне могу применять такой способ и совсем не вызывать метод glMaterial fv. Какой способ задействовать вам – решаете только вы.

Определяем нормали

Чтобы в OpenGL ES работало освещение, необходимо определить нормали вершин для каждой вершины модели. Нормаль вершины должна представлять собой единичный вектор, указывающий (обычно) в ту сторону, в которую повернута поверхность, к которой принадлежит вершина. На рис. 11.5 проиллюстрированы нормали вершин для нашего куба.

Рис. 11.5. Нормали вершин для каждой вершины нашего куба

Нормаль вершины – это еще один атрибут вершины, такой же, как позиция или цвет. Чтобы воспользоваться нормалями вершин, нам необходимо еще раз изменить класс Verti ces3. Для того чтобы указать OpenGL ES, где он может найти нормали для каждой вершины, мы будем использовать метод gl Normal PointerO, точно так же, как мы ранее применяли методы gl VertexPointer или gl Col or Pointer . В листинге 11.6 показана финальная версия класса Verti ces3.

Листинг 11.6. Класс Vertices3.Java, финальная версия, поддерживающая нормали

В классе появился новый член hasNormal.s, отслеживающий, имеют ли вершины нормали.

Конструктор теперь принимает также параметр hasNormals. Нам еще необходимо модифицировать расчет члена vertexSize, добавив три числа с плавающей точкой на каждую вершину там, где это возможно.

Как вы можете видеть, методы setVertices и setlndices остаются без изменений.

В только что продемонстрированном методе bind О используем те же приемы с буфером ByteBuffer, что и ранее, но в этот раз добавляем нормали с помощью метода gl Normal Pointer . Для вычисления смещения указателя нормали необходимо принять в расчет то, заданы ли координаты текстур и цвета.

Как вы можете видеть, метод draw также не изменился; все действо происходит в методе bind О.

Наконец, мы несколько изменяем метод unbindO. Запрещаем использование указателей нормали, если таковые имелись, соответственно очищая состояние OpenGL ES.

Применить измененный класс Verti ces3 так же просто, как и ранее. Рассмотрим небольшой пример:

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

Все, что нам остается, – создать экземпляр класса Verti ces3 и установить значения вершин. Довольно легко, не правда ли?

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

Собираем все воедино

Соберем все вместе. Нам необходимо нарисовать сцену, имеющую глобальную подсветку, точечные и направленные источники света. Они будут освещать куб, расположенный в начале координат. Нам также нужно вызвать метод gl uLookAt, . чтобы расположить камеру. На рис. 11.6 показан внешний вид нашего мира.

Как и для всех прочих примеров, создадим класс, который будет называться LightTest, как обычно расширяющий класс GLGame. Он будет возвращать экземпляр класса LightScreen с помощью метода getStartScreenO. Класс LightScreen наследует от класса GLScreen (листинг 11.7).

Рис. 11.6. Наша первая освещенная сцена

Листинг 11.7. Фрагменты класса LightTest.java. создание освещения с помощью OpenGL ES

Начнем с описания нескольких членов класса. Член angle хранит информацию о текущем угле поворота куба вокруг оси у. Член Verti ces3 хранит вершины модели куба, которые мы скоро определим. В дополнение у нас есть экземпляры классов AmbientLight, PointLight и Di rectional Light, а также экземпляр класса Material.

Далее следует конструктор. Здесь создаются вершины модели куба, а также загружается текстура ящика. Мы также инициализируем источники освещения и материалы. Цвет подсветки – светло-зеленый. Направленный источник – красного цвета и располагается в точке (3; 3; 0) нашего мира. Направленный источник света имеет синий рассеянный цвет, свет падает слева. Для материала используем значения по умолчанию (несколько фоновый, белый для рассеянной составляющей и черный для зеркальной).

В методе resume убеждаемся, что наша текстура (пере)загружается, если контекст будет потерян.

Метод createCube практически не изменился с предыдущих примеров. Однако в этот раз мы добавляем нормали для каждой вершины, что показано на рис. 11.5. Помимо этого все остается прежним.

В методе update просто увеличиваем угол поворота куба.

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

Далее мы устанавливаем матрицу проекций равной перспективной матрице проекций с помощью метода gl uPerspecti ve, а также используем метод gl uLookAt для модельно-видовой матрицы, благодаря чему камера работает так же, как на рис. 11.6.

Затем разрешаем использование освещения. К этому моменту еще не определен ни один источник света, поэтому мы задаем их в следующих нескольких строках с помощью вызова метода для источников света и материалов.

Как обычно, также разрешаем текстурирование и привязываем текстуру ящика. Наконец, вызываем метод gl RotatefC) для поворота куба и затем отрисовываем его вершины с помощью удачно размещенных вызовов экземпляра класса Verti ces3.

В конце метода мы отключаем точечные и направленные источники освещения (помните, подсветка – это глобальное состояние), а также текстурирование и тестирование глубины. Это все, что касается освещения в OpenGL ES.

Остальная часть класса пуста; нам не нужно производить какие-либо действия в случае паузы. На рис. 11.7 показан результат работы программы.

Рис. 11.7. Сцена, изображенная на рис. 11.6, отрисованная с помощью OpenGL ES

Несколько примечаний к освещению в OpenGL ES

Хотя использование освещения может добавить изюминку вашей игре, у него есть свои ограничения и ловушки. Есть несколько моментов, о которых вы должны знать.

Использование освещения потребляет слишком много ресурсов, особенно это заметно на медленных устройствах. Применяйте освещение осторожно. Чем больше источников освещения вы опишете, тем больше вычислений потребуется, чтобы отрисовать сцену.

Определять позицию/направление точечных/направленных источников света следует после того, как будут загружены матрицы камеры и до того, как модельно-видовая матрица будет умножена на какие-либо другие матрицы для перемещения и поворота объектов. Это критично. Если не следовать этим указаниям, возможно появление необъяснимых световых артефактов.

При использовании метода gl Seal ef для изменения размера модели ее нормали также будут масштабированы. Это плохо, поскольку OpenGL ES ожидает, что нормали будут иметь параметры в заданных единицах измерения. Чтобы обойти эту проблему, вы можете использовать команду glEnable(GL10.GL NORMALIZE) или при некоторых обстоятельствах gl Enable(GL10 .GL RESCALE N0RMAL). Полагаю, следует использовать первую команду, поскольку применение второй имеет ограничения и подводные камни. Проблема заключается в том, что нормализация или повторное масштабирование нормалей требует большой вычислительной мощности. Лучшее решение с точки зрения производительности – не масштабировать освещенные объекты.

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

  • Каксделатьтак,чтобыобъектбылярчекогданаходитсяближекисточникусвета.
  • Каксделатьотблескикогдамывидимотраженныйсветнапредмете(specular lighting )
  • Как сделать, чтобы объект был немного затененный, когда свет падает не прямо на объект(diffuse lighting)
  • Подсветка сцены(ambient lighting)
  • Тени. Эта тема заслуживает отдельного урока(или уроков, если даже не книг).
  • Зеркальное отражение(например, вода)
  • Подповерхностное рассеивание(например, как у воска)
  • Анизотропные материалы(окрашенный металл, например)
  • Затенение основанное на физических процессах, чтобы имитировать реальность еще лучше.
  • Преграждениесвета(Ambient Occlusion есличто-топреграждаетсвет,тостановитсятемнее)
  • Отражение цвета(красный ковер будет делать белый потолок слегка слегка красноватым)
  • Прозрачность
  • Глобальное освещение(в принципе все что мы указали выше можно назвать этим термином)

Другими словами, самое простое освещение и затенение.

Нормали

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

Нормали Треугольников

Нормаль к плоскости — это единичный вектор который направлен перпендикулярно к этой плоскости.

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

Вот псевдокод вычисления нормали:

треугольник(v1, v2, v3)
сторона1 = v2-v1
сторона2 = v3-v1
треугольник.нормаль = вектПроизведение(сторона1, сторона2).нормализировать()

Вершинная Нормаль

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

вершина v1, v2, v3, ....
треугольник tr1, tr2, tr3 // они все используют вершину v1
v1.нормаль = нормализовать(tr1.нормаль + tr2.нормаль + tr3.нормаль)

Использование нормалей вершин в OpenGL

Использовать нормали в OpenGL очень просто. Нормаль — это просто атрибут вершины, точно так же, как и позиция, цвет или UV координаты...Тоесть ничего нового учить не придется...даже наша простенькая функция loadOBJ уже загружает нормали.

GLuint normalbuffer;
glGenBuffers(1, &normalbuffer);

glBufferData(GL_ARRAY_BUFFER, normals.size() * sizeof(glm::vec3), &normals, GL_STATIC_DRAW);

// Третий атрибутный буфер: нормали
glEnableVertexAttribArray(2);

glBindBuffer(GL_ARRAY_BUFFER, normalbuffer);
glVertexAttribPointer(
2, // атрибут
3, // размер
GL_FLOAT, // тип
GL_FALSE, // нормализованный ли?
0, // шаг
(void*)0 // смещение в буфере
);

И этого достаточно чтобы начать:


Диффузное освещение

Важность нормали к поверхности

Когда световой луч попадает на поверхность, большая его часть отражается во все стороны. Это называется «диффузная компонента». Остальные компоненты мы рассмотрим чуть позже.

После падения луча, поверхность отражает свет по разному, в зависимости от угла под которым падает этот луч к поверхности. Если луч падает перпендикулярно к поверхности, то он концентрируется на маленьком участке, если по касательной, то рассеивается на гораздо большей поверхности:


С точки зрения компьютерной графики, цвет пикселя очень зависит от разности углов направления света и нормали поверхности.


//
//
float cosTheta = dot(n,l);

В этом коде «n» - это нормаль, а «l» - единичный вектор который идет от поверхности к источнику света(а не наоборот, хотя это может показатьсянепонятным)

Будьте внимательны со знаком

Иногда наша формула будет не работать. Например, когда свет будет находиться за треугольником, n и l будут противоположны, поэтому n.l будет отрицательным. И в итоге у нас будет какой-то отрицательный цвет, и в итоге какой-то бред. Поэтому мы приведем все отрицательный числа к 0 с помощью функции clamp.

// Косинус угла между нормалью и направлением света
// 1 — если свет перпендикулярен к треугольнику
// 0 — если свет параллелен к треугольнику
// 0 — если свет позади треугольника
float cosTheta = clamp(dot(n,l), 0,1);
color = LightColor * cosTheta;

Цвет материала

Конечно цвет предмета должен очень сильно зависеть от цвета материала. Белый свет состоит из трех компонент — красного, синего и зеленого. Когда свет падает на красную поверхность, то зеленая и синяя компоненты поглощаются, а красная отражается.



Мы можем промоделировать это простым умножением:

color = MaterialDiffuseColor * LightColor * cosTheta;

Моделирование света

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

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

color = MaterialDiffuseColor * LightColor * cosTheta / (distance*distance);

Вскоре нам понадобится еще один параметр чтобы управлять уровнем силы света — цвет света, но пока, давайте предположим, что у нас есть лампочка белого света с определенной мощностью(например, 60 ватт).

color = MaterialDiffuseColor * LightColor * LightPower * cosTheta / (distance*distance);

Объединяем все вместе

Чтобы этот код работал нам нужен определенный набор параметров(цвета и мощности) и немного дополнительного кода.

MaterialDiffuseColor — мы можем взять прямо из текстуры.

LightColor и LightPower нужно будет выставить в шейдере с помощью GLSL uniform.

CosTheta будет зависеть от векторов n и l. Его можно вычислять для любого из пространств, угол будет одним и тем же. Мы будем использовать пространство камеры, так как тут очень просто посчитать положение светового источника:

// Нормаль фрагмента в пространстве камеры
vec3 n = normalize(Normal_cameraspace);
// Направление света(от фрагмента к источнику света
vec3 l = normalize(LightDirection_cameraspace);

Normal _cameraspace и LightDirection _ cameraspace подсчитываются в вершинном шейдере и передаются во фрагментный для дальнейшей обработки:

// Позиция вершины в пространстве камеры:МВП * положение
gl_Position = MVP * vec4(vertexPosition_modelspace,1);
// Положение вершины в мировом пространстве: M * положение
Position_worldspace = (M * vec4(vertexPosition_modelspace,1)).xyz;
// Вектор который идет от вершины камере в пространстве камеры
// В пространстве камеры, камера находится по положению (0,0,0)
vec 3 vertexPosition _ cameraspace = ( V * M * vec 4( vertexPosition _ modelspace ,1)). xyz ;
EyeDirection_cameraspace = vec3(0,0,0) - vertexPosition_cameraspace;
// Вектор который идет от вершины к источнику света в пространстве камеры.
//Матрица M пропущена, так как она в в этом пространстве единичная.
vec3 LightPosition_cameraspace = (V * vec4(LightPosition_worldspace,1)).xyz;
LightDirection_cameraspace = LightPosition_cameraspace +
EyeDirection_cameraspace;
// Нормаль вершины в пространстве камеры
Normal_cameraspace = (V * M * vec4(vertexNormal_modelspace,0)).xyz; // Будет работать лишь в том случае , когда матрица модели не изменяет её размер .

На первый взгляд код может показаться довольно сложным и запутанным, но на самом деле, тут нет ничего нового чего не было в уроке 3: Матрицы. Я старался давать каждой переменной осмысленные имена, чтобы вам было легко понять что и как тут происходит.

Обязательно попробуйте!!!

M и V – это матрицы Модели и Вида, которые передаются в шейдер точно так же, как и наша старая добрая MVP.

Время испытаний

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

Результат

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



Вроде бы как получше, чем было раньше, но многого еще не хватает. Особенно заметна проблема с неосвещенными частями. Затылок нашей дорогой мартышки Сюзанны полностью черный(мы ведь использовали clamp()).

Окружающее освещение(ambient lighting)

Окружающее освещение – это чистой воды читерство.

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

Однако это слишком вычислительно затратно делать в реальном времени. И именно поэтому мы будем добавлять некую постоянную составляющую. Как будто сам объект излучает немного света, чтобы не быть полностью черным.

vec3 MaterialAmbientColor = vec3(0.1,0.1,0.1) * MaterialDiffuseColor;
color =
// Окружающее освещение : симулируем непрямое освещение
MaterialAmbientColor +
// Диффузное : " цвет " самого объекта
MaterialDiffuseColor * LightColor * LightPower * cosTheta /
(distance*distance);

Результат

Вот так вот будет немного лучше. Вы можете по игратьсяс коефициентами (0.1, 0.1, 0.1) чтобы попробовать добиться лучшего результата.



Отраженный свет(Specular light)

Часть света которая отражается, в основном отражается в сторону отраженного луча к поверхности.



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

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


// вектор взгляда(в сторону камеры)
vec3 E = normalize(EyeDirection_cameraspace);
//Направление в котором треугольник отражает свет
vec 3 R = reflect (- l , n );
// Косинус угла между вектором взгляда и вектором отражения обрезанный до нуля если нужно
// - Смотрим прям на отражение -> 1
// -Смотрим куда-то в другую сторону -> < 1
float cosAlpha = clamp(dot(E,R), 0,1);
color =
// Окружающее освещение:симулируем непрямое освещение
MaterialAmbientColor +
// Диффузное : " цвет " самого объекта
MaterialDiffuseColor * LightColor * LightPower * cosTheta /
(distance*distance) ;
// Отраженное: отраженные отблески, как зеркало
MaterialSpecularColor * LightColor * LightPower * pow(cosAlpha,5) /

В следующем уроке мы будем разбирать, как можно ускорить рендеринг нашего VBO.