Как работать с освещением на opengl
Перейти к содержимому

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

  • автор:

OpenGL — Освещение и все что с ним связано

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

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

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

float ambient[4] = ; . glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient);

Попробуйте изменить параметры и посмотрите на результат. Нулевую лампу (glEnable(GL_LIGHT0)) лучше отключить.

Исходный файл смотрите здесь. Исполняемый файл здесь.

6.3 Материал

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

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 оба рассеянных света

Ambient и diffuse переводятся на русский как «рассеянный». Разница между ними не очень понятна. Я использую только GL_DIFFUSE. Третий параметр определяет цвет соответствующего света, кроме случая GL_SHININESS. Цвет задается в виде массива из четырех элементов — RGBA. В случае GL_SHININESS params указывает на число типа float, которое должно быть в диапазоне от 0 до 128. Я написал простенький пример с цилиндром и раскрасил его грани в разные цвета. Вам надо всего лишь модифицировать функцию display.

void CALLBACK display(void) < GLUquadricObj *quadObj; GLfloat front_color[] = ; GLfloat back_color[] = ; 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);

Исходный файл смотрите здесь. Исполняемый файл здесь.

6.4 Лампы и их свойства

Все параметры лампы задаются с помощью функции glLight, которая имеет следующий прототип:

void glLight[if][v]( GLenum light, GLenum pname, GLfloat param)

Первый аргумент определяет номер лампы. Его можно задавать двумя способами. Первый — явно указать GL_LIHGTi, где GL_LIGHTi предопределено в файле gl.h следующим образом:

/* LightName */ #define GL_LIGHT0 0x4000 #define GL_LIGHT1 0x4001 #define GL_LIGHT2 0x4002 #define GL_LIGHT3 0x4003 #define GL_LIGHT4 0x4004 #define GL_LIGHT5 0x4005 #define GL_LIGHT6 0x4006 #define GL_LIGHT7 0x4007

Второй способ — GL_LIGHT0 + i, где i номер лампы. Такой способ используется, когда вам надо в цикле изменять параметры ламп. Второй аргумент определяет имя параметра, а третий его значение. Я здесь не буду перечислять всевозможные параметры и их допустимые значения, для этого есть справочник и MSDN. Я покажу и прокомментирую лишь небольшой пример, использующий три лампы. Для начала, давайте разберем шаблонный пример. С помощью следующих функций разрешаем освещение и включаем нулевую лампу.

glEnable(GL_LIGHTING); glEnable(GL_LIGHT0);

Массивы pos и dir содержат координаты местоположения лампы и направления, куда она светит. Массив dir содержит три координаты — x,y,z. Массив pos — четыре, назначение четвертого мне не очень ясно. Если его значение отличается от нуля, то изображение вполне логичное получается. Если же он ноль, то получается что-то непотребное.

glLightfv(GL_LIGHT0, GL_POSITION, pos); glLightfv(GL_LIGHT0, GL_SPOT_DIRECTION, dir);

Создайте новый проект с именем lamps. Скопируйте шаблонный файл glaux.c. Отредактируйте функцию main:

void main() < float pos[4] = ; float color[4] = ; float sp[4] = ; float mat_specular[] = ; auxInitPosition( 50, 10, 400, 400); auxInitDisplayMode( AUX_RGB | AUX_DEPTH | AUX_DOUBLE ); auxInitWindow( "Glaux Template" ); auxIdleFunc(display); auxReshapeFunc(resize); glEnable(GL_DEPTH_TEST); glEnable(GL_COLOR_MATERIAL); glEnable(GL_LIGHTING); glEnable(GL_LIGHT3); glEnable(GL_LIGHT5); glEnable(GL_LIGHT6); glLightfv(GL_LIGHT3, GL_SPECULAR, sp); glLightfv(GL_LIGHT5, GL_SPECULAR, sp); glLightfv(GL_LIGHT6, GL_SPECULAR, sp); color[1]=color[2]=0; glLightfv(GL_LIGHT3, GL_DIFFUSE, color); color[0]=0; color[1]=1; glLightfv(GL_LIGHT5, GL_DIFFUSE, color); color[1]=0; color[2]=1; glLightfv(GL_LIGHT6, GL_DIFFUSE, color); glLightfv(GL_LIGHT3, GL_POSITION, pos); pos[0] = -3; glLightfv(GL_LIGHT5, GL_POSITION, pos); pos[0]=0;pos[1]=-3; glLightfv(GL_LIGHT6, GL_POSITION, pos); glMaterialfv(GL_FRONT, GL_SPECULAR, mat_specular); glMaterialf(GL_FRONT, GL_SHININESS, 128.0); auxMainLoop(display); >

Здесь надо сделать пояснения для вызовов функции glLight с параметрами GL_DIFFUSE и GL_SPECULAR. GL_DIFFUSE — определяет цвет света источника. В данном примере у нас три лампы — с красным источником света, с зеленым и с синим. GL_SPECULAR определяет отраженный свет, см. предыдущий пункт.

Теперь измените функцию display:

glColor3d(1,1,1); auxSolidSphere(2);

Исходный файл смотрите здесь. Исполняемый файл здесь.

6.5 Тени

Тени напрямую не поддерживаются библиотекой OpenGL, поэтому этот материал для второй части книги. А если честно, то я с ними пока еще не разбирался.;-)

learnopengl. Урок 2.2 — Основы освещения

Распространение света в реальном мире это чрезвычайно сложное явление, зависящее от слишком многих факторов, и, располагая ограниченными вычислительными ресурсами, мы не можем себе позволить учитывать в расчетах все нюансы. Поэтому освещение в OpenGL основано на использовании приближенных к реальности упрощенных математических моделей, которые выглядят достаточно похожими, но рассчитываются гораздо проще. Эти модели освещения описывают физику света исходя из нашего понимания его природы. Одна из этих моделей называется моделью освещения по Фонгу (Phong). Модель Фонга состоит из трех главных компонентов: фонового (ambient), рассеянного/диффузного (diffuse) и бликового (specular). Ниже вы можете видеть, что они из себя представляют:

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

  1. Цвета
  2. Основы освещения
    • Фоновое освещение
    • Диффузное освещение
    • Вектора нормалей
    • Вычисление диффузного цвета
    • Еще кое-что
    • Освещение зеркальных бликов
    • Упражнения
  3. Материалы
  4. Карты освещения
  5. Источники света
  6. Множественное освещение
  • Фоновое освещение: даже в самой темной сцене обычно всегда есть хоть какой-нибудь свет (луна, дальний свет), поэтому объекты почти никогда не бывают абсолютно чёрными. Чтобы имитировать это, мы используем константу окружающего освещения, которая всегда будет придавать объекту некоторый оттенок.
  • Диффузное освещение: имитирует воздействие на объект направленного источника света. Это наиболее визуально значимый компонент модели освещения. Чем большая часть поверхности объекта обращена к источнику света, тем ярче он будет освещен.
  • Освещение зеркальных бликов: имитирует яркое пятно света (блик), которое появляется на блестящих объектах. По цвету зеркальные блики часто ближе к цвету источника света, чем к цвету объекта.

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

Фоновое освещение

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

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

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

void main()

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

Фоновое компонента освещения Phong

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

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

Диффузная компонента освещения Phong

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

Возможно, вы помните из урока по трансформациям, что чем меньше угол между двумя единичными векторами, тем больше результат скалярного произведения стремится к значению 1.0. Когда угол между обоими векторами составляет 90 градусов, скалярное произведение этих векторов становится равным 0. То же самое относится и к углу Θ: чем больше становится Θ, тем меньшее влияние оказывает источник света на цвет фрагмента.

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

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

Итак, что нам нужно для расчета диффузного освещения?

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

Вектора нормалей

Вектор нормали — это (единичный) вектор, перпендикулярный поверхности, построенной на данной вершине. Так как вершина сама по себе не имеет поверхности (это всего лишь точка в пространстве), то для нахождения вектора нормали используются соседние вершины. Для вычислении нормалей вершин куба мы можем сделать небольшую хитрость и применить к граням векторное произведение, но поскольку куб по форме представляет собой довольно простую фигуру, то мы добавим нормали к данным вершин вручную. Обновленный массив вершинных данных можно найти здесь. Попытайтесь вообразить нормали в виде векторов, направленных перпендикулярно поверхностям плоскостей куба (куб состоит из 6 плоскостей).

Поскольку мы добавили дополнительные данные в массив вершин, то нам необходимо обновить вершинный шейдер освещения:

#version 330 core layout (location = 0) in vec3 position; layout (location = 1) in vec3 normal; . 

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

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0); glEnableVertexAttribArray(0);

Для каждой вершины мы хотим использовать только первые 3 float-значения, а последние 3 значения пропускать, поэтому нам нужно только обновить параметр шага (stride) до величины, равной 6 размерам переменной типа GLfloat, и все.

Работа с массивом вершин, в котором шейдер использует не все данные, может показаться не неэффективной, но эти данные уже были загружены в память GPU из массива объекта-контейнера, поэтому никаких новых данных нам загружать не нужно. На практике такой подход более эффективен по сравнению с созданием для лампы собственного нового VBO.

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

out vec3 Normal; void main()

Осталось только объявить соответствующую входную переменную во фрагментном шейдере:

in vec3 Normal;

Вычисление диффузного цвета

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

uniform vec3 lightPos;

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

GLint lightPosLoc = glGetUniformLocation(lightingShader.Program, "lightPos"); glUniform3f(lightPosLoc, lightPos.x, lightPos.y, lightPos.z); 

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

out vec3 FragPos; out vec3 Normal; void main()

И, наконец, добавьте соответствующую входную переменную во фрагментный шейдер:

in vec3 FragPos;

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

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

vec3 norm = normalize(Normal); vec3 lightDir = normalize(lightPos - FragPos);

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

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

float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * lightColor;

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

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

vec3 result = (ambient + diffuse) * objectColor; color = vec4(result, 1.0f);

Если ваше приложение (и шейдеры) скомпилированы успешно, вы увидите что-то вроде этого:

Фоновая и рассеянная компоненты освещения Phong

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

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

Еще кое-что

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

Во-первых, вектора нормалей это только направления, и они не представляют собой определенных позиции в пространстве. Кроме того, у нормалей нет гомогенной компоненты (w-компонента положения вершины). Это означает, что производимые перемещения модели не должны влиять на вектора нормалей. Поэтому, если мы хотим умножить нормали на матрицу модели, то должны удалить часть матрицы, отвечающей за перемещения, и взять только левую верхнюю матрицу размером 3×3 (заметьте, мы могли бы установить w-компоненту вектора нормали в 0.0 и умножить его на целую матрицу 4×4, что также устранит воздействие значений сдвигов). Таким образом мы применим к векторам нормалей, только преобразования масштаба и поворота.

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

Неоднородное масштабирование нормали

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

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

Матрица нормалей определяется как «транспонированная обратная подматрица 3х3 левого верхнего угла матрицы модели». Уфф, это уже слишком, и если вы не совсем понимаете, что всё это значит, не переживайте; обратные и транспонированные матрицы мы еще не обсуждали. Обратите внимание, что во многих обучающих примерах матрица нормалей вычисляется применением вышеописанных операций к матрице модели-вида, но поскольку мы работаем в мировом пространстве (а не в пространстве вида), то используем только одну матрицу модели.

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

Normal = mat3(transpose(inverse(model))) * normal;

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

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

Освещение зеркальных бликов

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

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

Схема освещения зеркальных бликов

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

Вектор просмотра — это еще одна дополнительная переменная, необходимая для рассчета освещения зеркальных бликов. Мы можем её вычислить используя мировые координаты точки зрения наблюдателя и положения фрагмента. Затем мы вычисляем интенсивность блика, умножаем ее на цвет освещения и добавляем к вычисленным ранее компонентам фонового и рассеянного освещения.

Мы решили производить расчеты освещения в мировом пространстве, но большинство людей предпочитают это делать в пространстве вида. Преимущество рассчетов освещения в координатах вида заключается в том, что позиция наблюдателя всегда находится в (0,0,0), и вычислять её не нужно. Тем не менее, я считаю, что вычисление освещения в мировых координатах в учебных целях более понятно. Если вы всё же хотите рассчитать освещенность в пространстве вида,
то вам нужно умножать все соответствующие вектора на матрицу вида (не забудьте также изменить и матрицу нормалей).

Чтобы получить координаты наблюдателя в мировом пространстве, мы просто берем вектор положения объекта камеры (которая, разумеется, и является наблюдателем). Так что давайте добавим еще одну uniform-переменную в шейдер фрагментов и передадим в него соответствующий вектор положения камеры:

uniform vec3 viewPos;
GLint viewPosLoc = glGetUniformLocation(lightingShader.Program, "viewPos"); glUniform3f(viewPosLoc, camera.Position.x, camera.Position.y, camera.Position.z);

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

float specularStrength = 0.5f;

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

vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm);

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

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

float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); vec3 specular = specularStrength * spec * lightColor;

Сначала вычисляется скалярное произведение векторов отражения и направления взгляда (с отсевом отрицательных значений), а затем результат возводится в 32-ю степень. Константное значение 32 задает силу блеска. Чем больше это значение, тем сильнее свет будет отражаться, а не рассеиваться, и тем меньше станет размер пятна блика. Ниже вы можете видеть изображение, демонстрирующее воздействие различных значений блеска на внешний вид объекта:

Зеркальные блики

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

vec3 result = (ambient + diffuse + specular) * objectColor; color = vec4(result, 1.0f);

Теперь мы рассчитали все компоненты освещения модели освещения Фонга. С положения вашей точки зрения вы должны увидеть что-то вроде этого:

Освещение куба по Фонгу

Здесь вы можете найти полный исходный код приложения, а тут вершинный и фрагментный шейдеры.

Модели освещения Фонга и Гуро

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

Если модель освещения Фонга реализована в вершинном шейдере, то она называется методом тонирования Гуро, а не Фонга. Обратите внимание, что из-за интерполяции освещение выглядит несколько шероховатым. Как видите, модель Фонга дает более сглаженные результаты освещения.

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

Упражнения

  • Сейчас наш источник света это скучная неподвижная лампа. Сделайте его движущимся вокруг сцены, используя функции sin или cos. Наблюдение за изменением освещения даст вам хорошее представление о модели Фонга: решение.
  • Попробуйте менять в модели освещения силы воздействия фоновой, рассеянной и зеркальной компонент, и посмотрите, как это отразится на результате. Также поэкспериментируйте с коэффициентом блеска. Попытайтесь понять, почему определенные значения дают соответствующие им визуальные эффекты.
  • Рассчитайте освещение по Фонгу в пространстве вида, а не в мировом: решение.
  • Реализуйте освещение методом Гуро, а не Фонга. Если вы все сделали правильно, то освещение куба должно выглядеть не совсем хорошо (особенно зеркальные блики). Объясните, почему у куба такой странный вид: решение.

Освещение в OpenGL

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

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

В зависимости от реализации 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[4] = ;
    .
    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[] = ;
    GLfloat back_color[] = ;
    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

Выполняя главы последовательно, вы ознакомитесь с основами синтаксиса C#, увидите, как просто создавать оконные приложения с помощью .net, познакомитесь с библиотекой Tao, которая обеспечивает поддержку OpenGl в среде .NET, изучите основы 2D визуализации, работу как с примитивами, так и принцип загрузки и построения сложных 3D моделей , экспортированных из 3D редакторов.

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

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

  • Все статьи
  • основные сведения (9)
  • .NET Framework (6)
  • синтаксис C# (4)
  • OpenGL (42)
  • TaoFramework (34)
  • 2D (14)
  • сплайны (2)
  • графические фильтры (5)
  • геометрические преобразования (2)
  • 3D (12)
  • тела вращения (2)
  • GLUT (2)
  • текстурирование (4)
  • DevIL (1)
  • загрузка трехмерных моделей (3)
  • ASE (2)
  • системы частиц (3)
  • обработка изображений (3)
  • OpenGL 4.0 (1)
  • эффекты окружения (1)
  • полноэкранный режим (1)
  • C# (5)
  • освещение (2)
  • материалы (1)
  • видео (2)
  • загрузка файлов (1)
  • камера (1)
  • звук (1)
  • мультимедиа (2)
  • DirectX.AudioVideoPlayback (1)
  • GLFW (1)
  • сглаживание (1)
  • консоль (1)
  • freetype (1)
  • символы (1)
  • текст (1)
  • шейдеры (2)
  • GLSL (2)
  • online (1)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *