Уроки по OpenGL с сайта OGLDev


Урок 19 - Отраженный свет

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

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

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

Итого 5 разных объектов, на которые нужно обратить внимание:

  • 'I' это случайный луч, падающий на поверхность (и генерирующий рассеянный свет).
  • 'N' нормаль к поверхности.
  • 'R' это луч света, отраженный от поверхности. Он симметричен 'I' относительно нормали, но направлен противоположно (стрелки указывают направление).
  • 'V' это вектор из точки, в которую падает луч до "глаза" (обозначает зрителя.
  • 'α' это угол между 'R' и 'V'.

Мы собираемся создать модель отраженного света используя угол 'α'. Идея отраженного света в том, что сила луча будет наибольшей на векторе 'R'. В этом случае 'V' идентичен 'R' и угол между ними равен 0. Как только зритель начинает уходить в сторону, угол растет. Мы же хотим, что бы яркость света падала при увеличении угла. Вы уже можете предположить, что мы будем использовать скалярное произведение для нахождения косинуса угла 'α'. Это будет служить нашим коэффициентом отражения в формуле. Когда 'α' равен 0, косинус 1, максимальный коэффициент, который мы можем получить. Как только 'α' начнет увеличиваться, косинус будет убывать, пока 'α' не достигнет 90 градусов, а косинус 0, тогда уже абсолютно не будет эффекта отражения. Когда 'α' больше чем 90 градусов, косинус отрицательный, и эффекта так же не будет. Это значит, что зритель целиком вне отраженного луча

Для подсчета 'α' нам потребуются и 'R' и 'V'. 'V' может быть найдет через вычитание позиции точки, в которую падает свет в мировом пространстве, из позиции зрителя (то же в мировом пространстве). Так как наша камера уже поддерживается в мировом пространстве, нам требуется только передать ее позицию в шейдер. Изображение выше упрощено в том, что у нас только 1 точка в которую падает свет. На самом деле, треугольник освещен целиком. Поэтому мы будем вычислять отраженный свет для каждого пикселя (так же, как мы делали для рассеянного света) и для этого потребуется позиция пикселя в мировом пространстве. Найти ее так же легко - мы можем преобразовать вершины в мировое пространство и позволить растеризатору интерполировать позицию пикселя в том же пространстве, и предоставить результат в фрагментный шейдер. В целом это так же, как и передача нормали в прошлом уроке.

Последняя вещь для расчетов - отраженный луч 'R' будет найден используя вектор 'I' (который предоставит приложение в шейдер). Посмотрим на изображение:

Вспомним, что вектор не имеет начальной позиции и все вектора, имеющие одинаковое направление и длину - эквивалентны. Поэтому вектор 'I' скопирован "вниз" относительно поверхности, и копия идентична оригиналу. Цель найти вектор 'R'. Основываясь на правиле сложения векторов, 'R' равен 'I'+'V'. 'I' уже известен, поэтому осталось найти 'V'. Заметим, что вектор, обратный нормали 'N', так же известен как '-N' и используя скалярное произведение между 'I' и '-N', мы можем найти значение вектора, который получился при проецировании 'I' на '-N'. Это значение равно половине 'V'. Так как 'V' имеет то же направление, что и 'N', мы можем найти 'V' через умножение 'N' (чья длина 1.0) на удвоенное значение вектора. Подведем итог:

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

Давайте выведем итоговую формулу отраженного света:

Мы начинаем с умножения цвета света на цвет поверхности. Аналогично и для фонового и рассеянного освещения. Результат умножается на интенсивность отражения материала ('M'). Материал, который не имеет отражающих свойств (например дерево) будет иметь интенсивность равной 0, что превратит результат вычислений в 0. Блестящие объекты, например из металла, могут иметь повышенную интенсивность отражения. После этого мы умножаем на косинус угла между отраженным лучом света и вектором в глаз. Заметим, что последняя часть в степени 'P'. 'P' называется "силой отражения" (specular power) или "коэффициентом блеска" (shininess factor). Он аналогичен интенсивности для усиленная на гранях, если на них падает свет. Следующее изображение покажет эффект, когда сила отражения равна 1 (слева) и 32 (справа):

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

Прямиком к коду!

lighting_technique.h:32

class LightingTechnique : public Technique
{
public:
...

    void SetEyeWorldPos(const Vector3f& EyeWorldPos);
    void SetMatSpecularIntensity(float Intensity);
    void SetMatSpecularPower(float Power);

private:
...
    GLuint m_eyeWorldPosLocation;
    GLuint m_matSpecularIntensityLocation;
    GLuint m_matSpecularPowerLocation;

lighting_technique.cpp:154

bool LightingTechnique::Init(){
    ...
    m_eyeWorldPosition = GetUniformLocation("gEyeWorldPos");
    m_matSpecularIntensityLocation = GetUniformLocation("gMatSpecularIntensity");
    m_matSpecularPowerLocation = GetUniformLocation("gSpecularPower");
    ...
    if (...
        m_eyeWorldPosition == 0xFFFFFFFF ||
        m_matSpecularIntensityLocation == 0xFFFFFFFF ||
        m_matSpecularPowerLocation == 0xFFFFFFFF) {
        return false;
    }
    ...
}

void LightingTechnique::SetMatSpecularIntensity(float Intensity)
{
    glUniform1f(m_matSpecularIntensityLocation, Intensity);
}

void LightingTechnique::SetMatSpecularPower(float Power)
{
    glUniform1f(m_matSpecularPowerLocation, Power);
}

void LightingTechnique::SetEyeWorldPos(const Vector3f& EyeWorldPos)
{
    glUniform3f(m_eyeWorldPosition, EyeWorldPos.x, EyeWorldPos.y, EyeWorldPos.z);
}

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

lighting_technique.cpp:33

out vec3 WorldPos0;

void main()
{
      gl_Position = gWVP * vec4(Position, 1.0);
      TexCoord0   = TexCoord;
      Normal0     = (gWorld * vec4(Normal, 0.0)).xyz;
      WorldPos0   = (gWorld * vec4(Position, 1.0)).xyz;
}

В вершинный шейдер выше добавилась всего 1 строка (последняя). Мировая матрица (которая была добавлена в предыдущем уроке для преобразования нормали) теперь используется для передачи мировой позиции вершины в фрагментный шейдер. Здесь можно увидеть интересный способ преобразования одной вершины (предоставляется в локальном пространстве) используя 2 разные матрицы и передавая результат отдельно друг от друга. Результат полных преобразований (матрицы мировой проекции) идет в системную переменную 'gl_Position', и GPU заботится о преобразованиях в пространство экранных координат, и используется для растеризатора. Результат частичных преобразований (только в мировое пространство) переходит в добавленный пользователем атрибут, который просто интерполируется, поэтому каждый пиксель, для которого вызывается фрагментный шейдер, получает свои собственные координаты в мировом пространстве. Этот способ очень прост и удобен.

lighting_technique.cpp:48

in vec3 WorldPos0;
.
.
.
uniform vec3 gEyeWorldPos;
uniform float gMatSpecularIntensity;
uniform float gSpecularPower;

void main()
{
      vec4 AmbientColor = vec4(gDirectionalLight.Color, 1.0f) * gDirectionalLight.AmbientIntensity;
      vec3 LightDirection = -gDirectionalLight.Direction;
      vec3 Normal = normalize(Normal0);

      float DiffuseFactor = dot(Normal, LightDirection);

      vec4 DiffuseColor  = vec4(0, 0, 0, 0);
      vec4 SpecularColor = vec4(0, 0, 0, 0);

      if (DiffuseFactor > 0) {
        DiffuseColor = vec4(gDirectionalLight.Color, 1.0f) *
                gDirectionalLight.DiffuseIntensity *
                DiffuseFactor;

        vec3 VertexToEye = normalize(gEyeWorldPos - WorldPos0);
        vec3 LightReflect = normalize(reflect(gDirectionalLight.Direction, Normal));
        float SpecularFactor = dot(VertexToEye, LightReflect);
        SpecularFactor = pow(SpecularFactor, gSpecularPower);
        if (SpecularFactor > 0) {
            SpecularColor = vec4(gDirectionalLight.Color, 1.0f)
                    * gMatSpecularIntensity *
                    SpecularFactor;
        }
      }

      FragColor = texture2D(gSampler, TexCoord0.xy) * (AmbientColor + DiffuseColor + SpecularColor);
}

В фрагментном шейдере несколько изменений. Добавились 3 новые переменные, которые хранят атрибуты, требуемые для вычислений отраженного света (позиция глаза, интенсивность отражения и сила). Фоновый цвет вычисляется без изменений. Затем рассеянный и отраженный цвета создаются равными 0. Оба получают значения отличные от 0, когда угол между поверхностью и светом меньше 90 градусов. Это после проверки коэффициента рассеивания (с прошлого урока).

Следующий шаг - это подсчет вектора из вершины в мировом пространстве до позиции зрителя (тоже в мировом пространстве). Мы делаем это через вычитание позиции вершины из позиции глаза, которая является uniform-переменной и одинакова для всех пикселей. Этот вектор требуется нормировать для скалярного произведения. Затем отраженный вектор вычисляется через вшитую функцию 'отражение' (вы можете попробовать вычислить его вручную основываясь на описании выше). Эта функция принимает 2 параметра - вектор света и нормаль к поверхности. Важно заметить, что используется оригинальный вектор, который падает на поверхность, а не обратный к нему, который используется для рассеянного коэффициента. Это очевидно из диаграммы выше. Далее мы вычисляем коэффициент отражения как косинус между отраженным лучом света и вектором в глаз (снова используя скалярное произведение).

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

main.cpp:134

m_pEffect->SetEyeWorldPos(m_pGameCamera->GetPos());
m_pEffect->SetMatSpecularIntensity(1.0f);
m_pEffect->SetMatSpecularPower(32);

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

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

powered byDisqus