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

Ранее мы рассматривали модели освещения по Ламберту и Фонгу. Но освещение не ограничивается этими моделями — их великое множество. Их разнообразие объясняется тем, что есть большая разница, как будет отражаться свет от ткани, растительности, воды, снега, почвы и так далее. Конечно, покрыть все эти модели — задача неподъемная (особенно, в рамках одного поста), поэтому остановимся лишь на нескольких самых популярных.



Модель Ламберта

Невозможно пройти мимо «классики» — модели Ламберта, одной из самых простых и самых легких моделей с точки зрения сложности вычисления. Подробно обсуждалась в основах освещения, поэтому демонстрирую только код и результат:


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


    attribute vec3 aVertexPosition;
    attribute vec3 aVertexNormal;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;
    uniform mat3 uNMatrix;

    varying vec4 vVertex;    
    varying vec3 vNormal;
    
    void main(void) {
        // рассчитываем нормаль и положение вершины с учетом трансформаций
        vNormal = normalize(uNMatrix * aVertexNormal);
        vVertex = uMVMatrix * vec4(aVertexPosition, 1.0);
    
        gl_Position = uPMatrix * vVertex;
    }


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


    uniform vec3 uColor;

    varying vec4 vVertex;
    varying vec3 vNormal;

    uniform mat4 uLightMatrix;
    uniform vec3 uPointLightLocation;
    
    void main(void) {
        // получаем вектор направления освещения
        vec4 lightLocation = uLightMatrix * vec4(uPointLightLocation, 1.0);
        vec3 lightDirection = lightLocation.xyz - vVertex.xyz;
        
        // нормализованный вектор направления освещения
        vec3 L = normalize(lightDirection);
        // нормализуем нормаль, переданную из вершинного шейдера
        vec3 N = normalize(vNormal);
        // находим силу света по Ламберту
        float lambertComponent = max(dot(N, -L), 0.0);
        // получаем итоговый цвет из цвета объекта и освещения
        vec3 diffuseLight = uColor * lambertComponent;
        gl_FragColor = vec4(diffuseLight, 1.0);
    }


Результат:

Модель Ламберта

Модель Фонга

Одна из самых распространенных моделей. Более реалистичная, по сравнению с моделью Ламберта, так как поддерживает блики. Также подробно описывалась в основах освещения, поэтому меньше слов и больше кода 🙂


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


    attribute vec3 aVertexPosition;
    attribute vec3 aVertexNormal;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;
    uniform mat3 uNMatrix;
    
    varying vec3 vNormal;
    varying vec4 vVertex;
    
    void main(void) {
        // рассчитываем нормаль и положение вершины с учетом трансформаций
        vNormal = normalize(uNMatrix * aVertexNormal);
        vVertex = uMVMatrix * vec4(aVertexPosition, 1.0);
    
        gl_Position = uPMatrix * vVertex;
    }


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


    uniform vec3 uColor;
    
    varying vec4 vVertex;
    varying vec3 vNormal;
    varying vec4 vFinalColor;
    
    uniform mat4 uLightMatrix;
    uniform vec3 uPointLightLocation;
    
    const float shiness = 30.0;
    
    // фоновое освещение остается постоянным
    const vec3 ambientLightColor = vec3(0.1, 0.1, 0.1);
        
    void main(void) {
        // получаем вектор освещения
        vec4 lightLocation = uLightMatrix * vec4(uPointLightLocation, 1.0);
        vec3 lightDirection = lightLocation.xyz - vVertex.xyz;
        
        // освещения по Ламберту в качестве диффузного освещения
        vec3 N = normalize(vNormal);
        vec3 L = normalize(lightDirection);
        float lambertComponent = max(dot(N, -L), 0.0);
        vec3 diffuseLight = uColor * lambertComponent;
        
        // рассчитываем блики
        vec3 eyeVec = -vec3(vVertex.xyz);
        vec3 R = normalize(eyeVec);
        vec3 E = reflect(L, N);
        float specular = pow(max(dot(E, R), 0.0), shiness);
        vec3 specularLight = uColor * specular; 
        
        // итоговый цвет
        vec3 sumColor = ambientLightColor + diffuseLight + specularLight;
        
        gl_FragColor = vec4(sumColor, 1.0);
    }


Результат:

Модель Фонга

Модель Блинна

Также известная как модель Блинна-Фонга, она является разновидностью модели Фонга. Вместо вычисления скалярного произведения между отраженным лучом и наблюдателем, в модели Блинна находится скалярное произведение среднего вектора между наблюдателем и источником света и вектора наблюдателя. А так как найти средний вектор между наблюдателем и источником света (фактически это просто нормализованная сумма двух векторов) с точки зрения вычислений проще, чем найти вектор отражения, модель Блинна более распространена в применении и используется во многих графических системах по умолчанию. Обратите внимание на вычисление halfwayVector — в этом заключается основное отличие от модели Фонга.


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


    attribute vec3 aVertexPosition;
    attribute vec3 aVertexNormal;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;
    uniform mat3 uNMatrix;
    
    varying vec3 vNormal;
    varying vec4 vVertex;
    
    void main(void) {
        // рассчитываем нормаль и положение вершины с учетом трансформаций
        vNormal = normalize(uNMatrix * aVertexNormal);
        vVertex = uMVMatrix * vec4(aVertexPosition, 1.0);
    
        gl_Position = uPMatrix * vVertex;
    }


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


    uniform vec3 uColor;
    
    varying vec4 vVertex;
    varying vec3 vNormal;
    varying vec4 vFinalColor;
    
    uniform mat4 uLightMatrix;
    uniform vec3 uPointLightLocation;
    
    const float shiness = 30.0;
    
    // фоновое освещение остается постоянным
    const vec3 ambientLightColor = vec3(0.1, 0.1, 0.1);
        
    void main(void) {
        // получаем вектор освещения
        vec4 lightLocation = uLightMatrix * vec4(uPointLightLocation, 1.0);
        vec3 lightDirection = lightLocation.xyz - vVertex.xyz;
        
        // освещение по Ламберту в качестве диффузного освещения
        vec3 N = normalize(vNormal);
        vec3 L = normalize(lightDirection);
        float lambertComponent = max(dot(N, -L), 0.0);
        vec3 diffuseLight = uColor * lambertComponent;
        
        // находим средний вектор между освещением и наблюдателем
        vec3 eyeVec = -vec3(vVertex.xyz);
        vec3 R = normalize(eyeVec);
        vec3 halfwayVector = normalize(-L + R);
        
        float specular = pow(max(dot(halfwayVector, N), 0.0), shiness);
        vec3 specularLight = uColor * specular; 
        
        // итоговый цвет
        vec3 sumColor = ambientLightColor + diffuseLight + specularLight;
        
        gl_FragColor = vec4(sumColor, 1.0);
    }


Блики выглядят немного более размыто по сравнению с Фонгом (исправляется большим значением shiness):

Модель Блинна

Сэл-шейдерная модель

Можно также встретить название тун-шейдерная модель. Эта модель дает изображение, имитирующее рисование вручную. Использование этой модели можно встретить в таких мультфильмах как Человек-паук, Футурама и других. Идея шейдера довольно простая: мы берем за основу модель Ламберта, а затем вводим резкие переходы в освещении от порога к порогу. Например, если посмотреть в коде фрагментного шейдера ниже, при значениях коэффициента Ламберта от 0.5 до 0.95 цвет умножается на коэффициент 0.7 — таким образом мы убираем плавное сглаживание в освещении.


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


    attribute vec3 aVertexPosition;
    attribute vec3 aVertexNormal;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;
    uniform mat3 uNMatrix;

    varying vec4 vVertex;    
    varying vec3 vNormal;
    
    void main(void) {
        // рассчитываем нормаль и положение вершины с учетом трансформаций
        vNormal = normalize(uNMatrix * aVertexNormal);
        vVertex = uMVMatrix * vec4(aVertexPosition, 1.0);
    
        gl_Position = uPMatrix * vVertex;
    }


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


    uniform vec3 uColor;

    varying vec4 vVertex;
    varying vec3 vNormal;

    uniform mat4 uLightMatrix;
    uniform vec3 uPointLightLocation;
    
    void main(void) {
        // получаем вектор направления освещения
        vec4 lightLocation = uLightMatrix * vec4(uPointLightLocation, 1.0);
        vec3 lightDirection = lightLocation.xyz - vVertex.xyz;
        
        // нормализованный вектор направления освещения
        vec3 L = normalize(lightDirection);
        // нормализуем нормаль, переданную из вершинного шейдера
        vec3 N = normalize(vNormal);
        // находим силу света по Ламберту
        float lambertComponent = max(dot(N, -L), 0.0);
        
        // делаем резкие границы между порогами освещения
        vec3 diffuseLight;
        if (lambertComponent > 0.95) {
            diffuseLight = uColor;
        } else if (lambertComponent > 0.5) {
            diffuseLight = uColor * 0.7;
        } else if (lambertComponent > 0.2) {
            diffuseLight = uColor * 0.2;
        } else {
            diffuseLight = uColor * 0.05;
        }
        gl_FragColor = vec4(diffuseLight, 1.0);
    }


Результат:

Сэл-шейдинг

Minnaert

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


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


    attribute vec3 aVertexPosition;
    attribute vec3 aVertexNormal;

    uniform mat4 uMVMatrix;
    uniform mat4 uPMatrix;
    uniform mat3 uNMatrix;

    varying vec4 vVertex;    
    varying vec3 vNormal;
    
    void main(void) {
        // рассчитываем нормаль и положение вершины с учетом трансформаций
        vNormal = normalize(uNMatrix * aVertexNormal);
        vVertex = uMVMatrix * vec4(aVertexPosition, 1.0);
    
        gl_Position = uPMatrix * vVertex;
    }


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


    uniform vec3 uColor;

    varying vec4 vVertex;
    varying vec3 vNormal;

    uniform mat4 uLightMatrix;
    uniform vec3 uPointLightLocation;
    
    const float k = 0.7;
    
    void main(void) {
        // получаем вектор направления освещения
        vec4 lightLocation = uLightMatrix * vec4(uPointLightLocation, 1.0);
        vec3 lightDirection = lightLocation.xyz - vVertex.xyz;
        
        // нормализованный вектор направления освещения
        vec3 L = normalize(lightDirection);
        // нормализуем нормаль, переданную из вершинного шейдера
        vec3 N = normalize(vNormal);
        
        // вектор наблюдателя
        vec3 eyeVec = -vec3(vVertex.xyz);
        vec3 R = normalize(eyeVec);
        
        // находим коэффициенты согласно формуле Minnaert
        float p1 = pow(max(dot(N, -L), 0.0), 1.0 + k);
        float p2 = pow(1.0 - dot(N, R), 1.0 - k);
    
        vec3 diffuseLight = uColor * p1 * p2;
        gl_FragColor = vec4(diffuseLight, 1.0);
    }


Результат:

Minnaert

Что ж, это были довольно простые модели освещения, которые под силу освоить новичку в мире WebGL (вроде меня 🙂 ). С гораздо более полной подборкой можно ознакомиться здесь. Некоторые модели освещения из этой подборки достаточно сложные и заслуживают отдельной статьи. Мне же осталось выложить демо-пример и на этом закончить.

Ссылки:

Модели освещения
Метки:    

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

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