Изучаем OpenGL ES2 под Андроид. Урок 3 — Делаем освещение реалистичнее. По-точечный расчет освещения.

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

Предположения и предпосылки

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

Что такое по-пиксельное(точечное) освещение?

По-пиксельное освещение является относительно новым явлением в играх благодаря появлению шейдеров. Многие известные старые игры, такие как Half-Life были разработаны до применения шейдеров и использовали в основном статическое освещение, с применением некоторых трюков для имитации динамического освещения с использованием вершинного свечения (фильтр Гуро) или других способов, таких как динамические карты освещения.

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

С применением шейдеров, многие из этих расчетов можно переложить на GPU(графический процессор), что дает возможность обрабатывать большее количество эффектов в режиме реального времени.

Переход от вершинного к фрагментному расчету освещения

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

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

Существенное различие между двумя этими типами освещения можно увидеть в определенных ситуациях. Посмотрите на следующие скриншоты:

вершинное освещениефрагментное освещение

вершинное освещение пример второйфрагментное освещение пример второй

Общее представление о вершинном освещении

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

Вершинный шейдер

uniform mat4 u_MVPMatrix;     // Константа содержащая комбинированную матрицу model/view/projection.
uniform mat4 u_MVMatrix;      // Константа содержащая комбинированную матрицу model/view.
uniform vec3 u_LightPos;      // Положение источника света в видимом пространстве.

attribute vec4 a_Position;    // Здесь храним информацию о положении вершин.
attribute vec4 a_Color;       // Здесь храним информацию о цвете.
attribute vec3 a_Normal;      // Здесь храним информацию о нормали.

varying vec4 v_Color;         // This will be passed into the fragment shader.

// Начало программы вершинного шейдера.
void main()
{
    // Преобразуем вершины к видимой области.
    vec3 modelViewVertex = vec3(u_MVMatrix * a_Position);

    // Преобразуем нормализованное положение к видимой области.
    vec3 modelViewNormal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));

    // Будет использоваться для расчета затухания.
    float distance = length(u_LightPos - modelViewVertex);

    // Получаем вектор направления от источника света к вершине.
    vec3 lightVector = normalize(u_LightPos - modelViewVertex);

    // Вычисляем скалярное произведение вектора света и вектора нормали. Если вектора совпадают в направлении,
    // то точка имеет максимальное освещение.
    float diffuse = max(dot(modelViewNormal, lightVector), 0.1);

    // Ослабление освещенности в зависимости от расстояния.
    diffuse = diffuse * (1.0 / (1.0 + (0.25 * distance * distance)));

    // Перемножаем цвет на уровень освещенности. Применяется к треугольникам.
    v_Color = a_Color * diffuse;

    // gl_Position переменная для хранения данных о конечном положении.
    // Перемножаем вершины и матрицу для получения конечного положения в нормализованных координатах экрана.
    gl_Position = u_MVPMatrix * a_Position;
}

Фрагментный шейдер

precision mediump float;       // Устанавливаем по умолчания среднюю точность.
                               // Высокая точность нам не нужна.
varying vec4 v_Color;          // Цвет вершинного шейдера интерполированного через треугольники. 
// Начало программы шейдера.
void main()
{
    gl_FragColor = v_Color;    // Передаем значение цвета в поток.
}

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

Реализация расчетов освещения в фрагментном шейдере

Вот как выглядит код после перехода к по-пиксельному расчету освещения.

Вершинный шейдер

uniform mat4 u_MVPMatrix;      // Константа содержащая комбинированную матрицу model/view/projection.
uniform mat4 u_MVMatrix;       // Константа содержащая комбинированную матрицу model/view.

attribute vec4 a_Position;     // Передаем сюда информацию о положении вершин.
attribute vec4 a_Color;        // Передаем сюда информацию о цвете.
attribute vec3 a_Normal;       // Передаем информацию о нормализованных вершинах.

varying vec3 v_Position;       // Передаем данные в фрагментный шейдер.
varying vec4 v_Color;          // Передаем данные в фрагментный шейдер.
varying vec3 v_Normal;         // Передаем данные в фрагментный шейдер.

// Начало программы вершинного шейдера.
void main()
{
    // Преобразуем вершины к видимому пространству.
    v_Position = vec3(u_MVMatrix * a_Position);

    // Передаем цвет.
    v_Color = a_Color;

    // Преобразуем нормализованное пространство к видимому пространству.
    v_Normal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));

    // gl_Position переменная содержащая окончательное значение о положении.
    // Перемножаем вершины и матрицу для получения конечных точек в нормализованном пространстве экрана.
    gl_Position = u_MVPMatrix * a_Position;
}

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

Фрагментный шейдер
precision mediump float;       // Устанавливаем по умолчания среднюю точность.
                            // Высокая точность нам не нужна.
uniform vec3 u_LightPos;       // Положение источника света в видимом простнастве.

varying vec3 v_Position;       // Интерполированное положение для фрагмента.
varying vec4 v_Color;          // Цвет вершинного шейдера интерполированный
                               // через треугольник.
varying vec3 v_Normal;         // Интерполированные нормали шейдера.

// Начало программы франгментного шейдера.
void main()
{
    // Расстояние для расчета ослабления света.
    float distance = length(u_LightPos - v_Position);

    // Получаем вектор направления от источника света к вершинам.
    vec3 lightVector = normalize(u_LightPos - v_Position);

    // Рассчитываем скалярное произведение вектора света и вектора нормали.
    // Если они совпадают в направлении мы получаем максимум освещения.
    float diffuse = max(dot(v_Normal, lightVector), 0.1);

    // Добавляем коэффициент затухание.
    diffuse = diffuse * (1.0 / (1.0 + (0.25 * distance * distance)));

    // Перемножаем цвет и коэффициент затухания для получения результирующего цвета.
    gl_FragColor = v_Color * diffuse;
}

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

Подведение итогов

Полный исходный код для этого урока можно загрузить с сайта проекта на GitHub.

Установочный файл приложения с этого урока можете скачать непосредственно с сайта Google Play.

 

 

 

 

 


					

Comments are closed.