Базовые модели освещения: Ламберт, Фонг, Блинн-Фонг

Вступительно слово

Здравствуйте. Эта статья - вступительная на этом сайте в разделе "компьютерная графика". Сегодня мы рассмотрим основные принципы затенения полигональных моделей в современной компьютерной графике на примере трёх базовых моделей затенения.

В русском сегменте интернета эти модели называются моделями освещения, что не вполне корректно, так как во-первых в анлийском языке они называются "shading model", что буквально переводится как "модель затенения", а во-вторых, в понятие "модель освещения" разумно включать все компоненты освещения, из которых складывается итоговая картинка: непрямое освещение (indirect lighting), рассеянное освещение (ambient occlusion), тени, отражения и т.д.

Инфраструктура

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

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

Первые шаги: выводим нормали

Для расчёта освещённости в каждой точке трёхмерной модели нам понадобится в первую очередь найти угол между направлением на источник света и нормалью к поверхности в данной точке.

Почему именно такой угол? Всё просто: нормаль к плоскости является (по определению) является перпендикуляром к этой плоскости. Чем ближе вектор падения света к этому перпендикуляру (то есть чем меньше угол между этими векторами) тем больше света попадает на объект: свет падает прямо. А вот чем этот угол больше, тем слабее будет итогова освещённость, так как свет падает под углом.

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

Shader "karonator/JustNormals"
{
    SubShader
    {
        Pass
        {
            CGPROGRAM

            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct vertex_data
            {
                float4 vertex: POSITION;
                float3 normal: NORMAL0;
            };

            struct fragment_data
            {
                float4 pos: POSITION;
                float3 normal: NORMAL0;
            };

            fragment_data vert(vertex_data v)
            {
                fragment_data o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.normal = v.normal;
                return o;
            }

            fixed4 frag(fragment_data i): COLOR
            {
                return float4(i.normal, 1.0);
            }

            ENDCG
        }
    }
}

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

[[ картинка ]]

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

Основы затенения: модель Ламберта

Перейдём к модели затенения Ламберта. Вообще. в любой модели затенения итоговая освещённость складывается обычно из трёх компонент:

  1. Фоновая составляющая (ambient)
  2. Рассеянная составляющая (diffuse)
  3. Зеркальная составляющая (specular)

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

(( формула ))

Бликовое затенение: модель Фонга