Урок 6. Установка источников освещения в OpenGL4

20.08.2012

В этом уроке вы узнаете: какие бывают типы источников света; каким образом устанавливается освещение в OpenGL и почему для создания источников света обязательны шейдеры; каким образом вычисляется освещение с точки зрения математики; как правильно написать шейдер для освещения объектов; какие еще бывают типы освещения и как это связано с шейдерами.

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

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

Создание источников света в OpenGL4

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

Итак, разберем обычный случай освещения – единичный источник света. Если вы изучали рисование то вы знаете, что чтобы правильно закрасить объект, надо его часть, обращенную к свету сделать светлой, а ту, куда свет не попадает темной. При этом есть четкая граница где начинается тень. Давайте переведем эту проблему на язык математики. Имеется поверхность X произвольной формы и источник света с координатами L.

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

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

Определив нужный угол, мы можем задать функцию от этого угла, результатом функции будет цвет точки объекта. Чем угол меньше (острее), тем ярче цвет. При угле 90 градусов объект перестает быть освещен. Если угол больше 90 градусов, то это темные, теневые участки объекта. Решим задачу.

Обозначим каждую точку поверхности X символом n, нормаль к поверхности обозначим N. Искомая формула будет следующая:

Треугольными скобками здесь мы обозначим операцию нормализации, то есть приведения вектора к длинне 1. Прямыми скобками обозначается операция определения модуля вектора, то есть величины вектора. Таким образом, мы имеем два вектора – вектор нормали к поверхности Nдля каждой точки и второй вектор – вектор направления луча от точки поверхности к источнику света – вектор L-n. Кстати, исходные данные для операции L-n является координаты двух точек, результат же операции – не координаты, а вектор. На самом деле, в этой формуле даже arcsinне обязателен, так как мы всего-лишь вычисляем цвет, а не траекторию полета на луну. И дело не в точности вычислений, а в том что просто нет необходимости вычислять каждый раз arcsin, к тому-же это может и замедлить шейдер. Так что формула расчета интенсивности цвета упрощается:

Ну а что делать если источников света несколько? На самом деле, эта формула будет прекрасно работать и в этом случае. Просто просуммируем все данные:

То есть мы просто просуммируем данные, подставляя для каждого источника света новые координаты и затем поделим результат на количество источников света. А вот информация к размышлению. Мы все сделали правильно, однако чем острее угол a, тем меньше результат, а значит и темнее цвет… Стоп. Что-то здесь не так… Исходные данные правильны и формулы правильны, но результат чуть не такой, как хотелось бы. Нам ведь нужно точку в освещенных местах делать светлой, а не темной. Всем известно, что любые формулы хороши тогда, когда их можно еще больше упростить. И если нечего уже упрощать, значит формула имеет окончательный вид и её нужно высечь на глиняной дощечке. Сделаем так:

Но 1-aэто как раз то, что нужно, ведь если а малое, то 1-а самое большое, а значит и цвет большой. Так что окончательно имеем:

и

Мы заменили операцию векторного умножения векторов, операцией скалярного умножения (crossproduct на dotproduct). К тому-же при скалярном умножении векторов получается не вектор а число, так что и модуль вектора вычислять не нужно. Вектор и так самостоятельно сократился, потеряв все данные о направлении, и оставив нужную нам длину. Получившаяся формула является хорошей информацией к размышлению, ведь одной небольшой формулой мы просчитали освещение объекта, освещаемого произвольным количеством источников. Неужели и правда всё так просто? Да, именно так просто. Теперь у нас есть данные для того, чтобы написать шейдер для освещения. Можно быть уверенным, что даже хорошо неподготвленный к шейдерам программист с легкостью поймет его с учетом текущего контекста беседы об освещении.

Определим тип входящих данных вершинного буфера:

layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec3 inNormal;
layout (location = 2) in vec2 inTex;

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

uniform vec3 vLightPos[5];
uniform vec3 vLightColor[5];

Теперь определим фрагмент вершинного шейдера, в котором мы будем рассчитывать освещенность объекта по формуле.

for (int i=0; i<5; i++)
outputColor+=dot(normalize(inNormal),normalize(vLightPos[i]-inPosition))*vLightColor[i]*0.5f;

Если исходные данные нормалей представляют из себя нормализованные вектора, то есть если их модуль равняется единица то вместо normalize(inNomal) можно написать просто inNormal.

Определение нормалей к поверхности

Остался нерешенным вопрос, каким образом в пиксельный шейдер передавать данные о нормали к поверхности. Точнее передавать их несложно, но откуда их брать? Дело в том, что данные нашей модели содержат лишь координаты вершин, нормалей там нет. Чтобы было более понятно посмотрим на рисунок части поверхности:

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

Для решения этой задачи зададимся вопросом: а насколько просто или сложно провести нормаль не к вершине, а к полигону, то есть к произвольному треугольнику поверхности. Решим эту задачу математически. Итак, имеется треугольник с набором вершин n1n2 и n3. Он образует плоскость. Необходимо определить перпендикуляр N к этой плоскости .

Решим задачу:

Таким образом, чтобы вычислить нормаль к полигону, нужно векторно умножить вектор n1n2 на вектор n1n3. Результатом такой операции получается вектор. Результирующий вектор перпендикулярен двум исходным. Чтобы окончательно решить задачу, остается только нормализовать нормали, так как полигоны могут быть различные по размеру и мы не хотим чтобы вектора нормалей тоже «скакали» по размеру. Приведенную выше формулу можно сразу перевести в код для нашей программы. Этот код мы будем использовать в приложении, а не в шейдере, так как в шейдере каждый раз рассчитывать нормали не имеет смысла. Мы можем один раз рассчитать их для нашей модели и затем использовать столько раз сколько понадобиться в шейдере.

glm::vec3 n[3];
glm::vec3 polynormal;
polynormal=glm::cross(n[0]-n[1],n[0]-n[2]);
polynormal=glm::(polynormal);

Исходными данными является массив из трех точек n[]. Это координаты полигона. Выходящими данными является нормализованный вектор polynormal– искомая нормаль к полигону. Теперь вернемся к рисунку нашей поверхности X. Как вы думаете, будет ли нормаль из вершины поверхности смотреть в туже сторону что и нормали из центров полигонов? Ответ – безусловно. Кроме того, Если вычислить среднее направление всех соседних с вершиной нормалей к полигонам, мы и получим искомую нормаль к вершине.

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

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

Хотя исходные данные для формулы несколько векторов N, вычисление среднего значения можно сделать также как и с обычными числами. Из урока 3 мы помним, что если вектор представить как одно число, то обнаружится много общего между числами и векторами. Таким образом, у векторов тоже определена операция вычисления среднего значения.  Остается отметить, что прилегающих к произвольной вершине полигонов не всегда ровно 6, это видно из рисунка поверхности X, если обратить внимание на вершины по краям фигуры. Если количество прилегающих полигонов меньше, то мы просто в формулу вместо 6 ставим количество прилегающих к вершине полигонов.

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

Унификация нормалей

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

Неправда ли, если у ежика некоторые иголки растут в обратную сторону, это крайне неприятная ситуация для ежика. Чтобы избежать такой ситуации воспользуемся таким алгоритмом. Сначала маркируем все полигоны как непомеченные. После этого возьмем любой полигон с правильной нормалью. Теперь найдем соседний к нему непомеченный полигон. Сравним правильную нормаль с той, что у полигона. Если направление оказалось противоположным – просто перевернем вектор нормали. Теперь пометим полигон как обработанный, затем возьмем любой обработанный полигон и соседний к нему необработанный. Так до тех пор, пока не перевернем все нормали в одну сторону.

Приложение для установки источников освещения

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

glm::vec3 LightPos[5];
glm::vec3 LightColor[5];

Теперь всё готово, чтобы при каждом вызове функции Renderустанавливать наши источники освещения. Сначала заполним их значениями. В качестве координат источников  света используем динамически меняющиеся орбитальные координаты:

glm::vec3 LightPos[5];
glm::vec3 LightColor[5];
LightPos[0]=glm::vec3(sin(orbit*f0)*r,cos(orbit*f0)*r,cos(orbit*f0)*r);
LightPos[1]=glm::vec3(sin(orbit*f1)*r,-sin(orbit*f1)*r,-cos(orbit*f1)*r);
LightPos[2]=glm::vec3(cos(orbit*f2)*r,sin(orbit*f2)*r,sin(orbit*f2)*r);
LightColor[0]=glm::vec3(1,1,0);
LightColor[1]=glm::vec3(0,0,1);
LightColor[2]=glm::vec3(0,1,1);

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

// Установка констант шейдера для источников
int iLightPos=  glGetUniformLocation(spMain.getProgramID(),"vLightPos");
int iLightColor=glGetUniformLocation(spMain.getProgramID(),"vLightColor");
glUniform3fv(iLightPos,5(GLfloat*)LightPos);
glUniform3fv(iLightColor,5,(GLfloat*)LightColor);

Теперь перейдем от нашего приложения к коду шейдера. Рассмотрим вершинныйшейдер для нашего приложения.

#version 330
 
uniform mat4 mWorld;
uniform mat4 mView;
uniform mat4 mProjection;
uniform vec3 vLightPos[5];
uniform vec3 vLightColor[5];
 
layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec3 inNormal;
layout (location = 2) in vec2 inTex;
 
smooth out vec3 theColor;
 
void main()
{
gl_Position = mProjection*mView*mWorld*vec4(inPosition, 1.0); vec3 outputColor=vec3(0.5f,0.5f,0.5f);
for (int i=0; i<3; i++)
outputColor+=dot(normalize(inNormal),normalize(vLightPos[i]-inPosition))*vLightColor[i]*0.5f;
theColor = outputColor;
}

Обратите внимание на строку, в которой в цикле рассчитывается переменная output.Color. Данная строка полностью соответствует формуле, рассмотренной ранее в данном уроке:

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

Обзор приложения

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

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

Различные типы источников освещения

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

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

  • Рассеянное освещение от купола неба в туманную погоду
  • Ясное направленное освещение лучами солнца
  • Эффекты вечернего освещения (цвет солнца меняется)
  • Фонари и различные источники света возле и на зданиях. Источники света в виде окон.
  • Источники света, отражающееся в водной поверхности.

Перечислим основные типы освещения, для элементов интерьера:

  • Обычные фонари на потолках помещений.
  • Источники света от горящих и вспыхивающих предметов.
  • Различные фонари, светильники, световые табло.

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

Более менее просто выглядит техническая сторона вопроса с направленным освещения солнца. Солнце – это единичный источник освещения. Один объект, одни координаты. Все лучи идут в одном направлении. Единственная сложность с солнцем – это добавить тени. Тени в играх — это также многогранная тема с точки зрения программирования. В некоторых случаях, если тени слишком сложно создать используется предварительно отрендеренные тени. К счастью, в OpenGL очень просто комбинировать текстуры, так что если карта теней уже есть, то можно её просто поместить в отдельную текстуру и скомбинировать две текстуры – одна будет для текстуры ландшафта(или стен интерьера), а другая – для теней.

Для интерьера характерны другие технические сложности. Во первых, когда моделируется сцена интерьера первое что бросается в глаза моделерам и программистам – это наличие большого количества источников света. Например, модель здания может насчитывать 75 внутренних светильников. Если в шейдере создан массив на 12 светильников и параметры быстродействия не позволяют задействовать больше, то как эти 75 светильников поместить и просчитать в сцене?

Другая сложность с интерьером это то, что на самом деле если в сцене пусть даже 6 светильников поблизости от текущего места действия, то реалистично отобразить их тоже не просто. Дело в том, что с точки зрения физики, все эти источники света не представляются именно 6-ю источниками с шестью координатами и т.п. С точки зрения физики луч от каждого из источников сначала падает на одну стену интерьера, потом на другую и т.п., пока интенсивность многократных отражений и рассеиваний не достигнет нуля. То есть, в сцене присутствует множество теней, мягких и жестких, и вообщем игра света настолько сложная, что просчитать её в реальном времени можно лишь очень и очень условно. Таким образом, освещение в играх было и остается отличной платформой для разнообразных экспериментов.

Заключение

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

Заполнен: OpenGL4
Присвоен тэг:

avatar

Об Авторе ()

Планета назначения, JO - 8703 - IV, обозначается светлой точкой на объемном экране корабля. К тому времени, как Робоид нанялся на юпитерианский круговой рейс, ему следовало бы стать главным инженером, но после Суховодного пионерского его вышибли, внеся в черный список, и высадили в Луна-Сити за то, что вместо слежения за приборами он провел время, программируя синтезатор пищи корабля строжайше запрещенной корабельными инструкциями последовательностью синтеза пива.

Комментирование закрыто.

Наверх