Пользовательское освещение.

Введение.

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

Используем пользовательскую функцию освещения.

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

// шейдер является поверхностным шейдером, что означает, что он будет расширен
// на Unity в фоновом режиме, чтобы иметь хорошее освещение и другие функции
// наша поверхностная шейдерная функция называется surf,
// и мы используем нашу собственную модель освещения
//fullforwardshadows гарантирует, что Unity добавит тени, которым может понадобиться шейдер
#pragma surface surf Custom fullforwardShadows
Затем добавим метод в наш шейдер, который будет нашей функцией освещения. Название этой функции должно быть LightingX, где X - это имя нашего метода освещения, на который мы ссылаемся в определении поверхности. В этом определении метода, получаем данные, который мы возвращаем из поверхностного шейдера, а также направление света, которое попадает на точку поверхности и затухание (я объясню позже, что это делает).
// наша функция освещения. Будет вызываться один раз за светильник
float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
    return 0;
}
Вы могли заметить, что я использую здесь структуру SurfaceOutput вместо структуры SurfaceOutputStandart. Это потому, что наша пользовательская модель освещения не будет использовать металличность и мягкость (softness), поэтому мы можем использовать структуру, предназначенную для материалов без PBR (вы можете использовать SurfaceOutputStandard для своих пользовательских функций освещения, если хотите, но вам придется импортировать UnityPBSLighting.cginc). Чтобы использовать структуру SurfaceOutput, мы также должны вернуть его в нашу функцию поверхностного шейдера и удалить части, где мы установили значения металла и гладкости.
Я также удалил металлы и гладкость из переменных и свойств шейдера, потому что мы больше не используем их, но это не важно.
// функция поверхностного шейдера, которая устанавливает параметры,
// которые затем использует функция освещения
void surf (Input i, inout SurfaceOutput o) {
    // образец и оттенок альбедо текстуры
    fixed4 col = tex2D(_MainTex, i.uv_MainTex);
    col *= _Color;
    o.Albedo = col.rgb;

    //o.Emission = _Emission;
}
У нас должна быть функция освещения, которую использует Unity, но она пока просто возвращает 0 (черный), поэтому мы не можем видеть никаких огней.
Причина, по которой мы все еще можем разглядеть фигуры, и модель не сплошная черная, в том, что Unity еще и рассчитывает глобальное освещение и пытается смоделировать окружающую среду (посмотрите на skybox). Если мы изменим освещение окружающей среды на черный на вкладке освещения, мы увидим нашу модель полностью черной, но наше освещение уже работает! Поэтому вы можете попробовать, что, по вашему мнению, заставляет вашу игру выглядеть так, как вы этого хотите.

Реализация затухания освещения.

Теперь мы реализуем простую модель освещения. Первым шагом является получение dot произведения между вектором от поверхности к свету и нормали к поверхности. К счастью, Unity дает нам и то и другое уже в мировом пространстве, а также нормализовано (они имеют длину 1), поэтому нам не нужно их преобразовывать.
dot продукт сообщает нам, насколько поверхность повернута к свету. Он имеет значение 0, если поверхность параллельна к направлению на свет, и значение 1, если нормаль указывает на свет и -1, если нормаль противоположна к направлению нас свет.
// наша функция освещения. Будет вызываться один раз за светильник
float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
    // насколько нормаль направлена на светильник
    float towardsLight = dot(s.Normal, lightDir);
    return towardsLight;
}
Метод освещения, который мы собираемся реализовать, довольно прост, но также очень универсален. Мы будем использовать количество, насколько поверхность указывает на свет, чтобы найти значение некоей текстуры и использовать это как нашу яркость.
Для этого мы должны изменить переменную из значений, которые идут от -1 до 1 до значений от 0 до 1 (поскольку UV-переменные в диапазоне от 0 до 1), мы делаем это, умножая его на 0,5 (тогда он имеет диапазон от - 0,5 до 0,5), а затем добавление 0,5 (смещение диапазона до 0 до 1, как мы этого хотим).
Затем мы добавляем новую текстуру в наш шейдер как переменную шейдера, а также свойство. Я назову его наклоном (Ramp), потому что техника освещения обычно называется toon ramp. Затем мы читаем эту текстуру в функции освещения и возвращаем значение, которое мы читаем. Я использую функцию, наполовину черную и наполовину белую, поэтому мы должны увидеть четкое обрезание модели.
//показываем значения для редактирования в инспекторе
Properties {
    _Color ("Tint", Color) = (0, 0, 0, 1)
    _MainTex ("Texture", 2D) = "white" {}
    [HDR] _Emission ("Emission", color) = (0,0,0)

    _Ramp ("Toon Ramp", 2D) = "white" {}
}

//...

sampler2D _Ramp;
Это текстура, которую я использую в примере:
// наша функция освещения. Будет вызываться один раз за светильник
float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
    // насколько нормаль направлена на светильник
    float towardsLight = dot(s.Normal, lightDir);
    // преобразуем значение от -1 до 1 к от 0 до 1
    towardsLight = towardsLight * 0.5 + 0.5;

    //читаем текстуру наклона
    float3 lightIntensity = tex2D(_Ramp, towardsLight).rgb;

    return float4(lightIntensity, 1);
}

Вы можете видеть, что мы все же имеем альбедо в тени здесь, это опять же из-за того, что Unity расчет окружающей среды добавляется в фоновом режиме, но в ближайшее время оно будет выглядеть лучше.
А именно, чтобы он выглядел лучше, мы собираемся умножить интенсивность света на альбедо материала, чтобы мы правильно видели наши цвета. Также добавим затухание, которое включает в себя отброшенные на объект тени и спад света. Поэтому свет становится темнее при удалении и на светлом цвете, и объект становится тонированным в цвете, в котором он освещается.
// наша функция освещения. Будет вызываться один раз за светильник
float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
    // насколько нормаль направлена на светильник
    float towardsLight = dot(s.Normal, lightDir);
    // преобразуем значение от -1 до 1 к от 0 до 1
    towardsLight = towardsLight * 0.5 + 0.5;

    // читаем текстуру наклона
    float3 lightIntensity = tex2D(_Ramp, towardsLight).rgb;

    //комбинируем цвета
    float4 col;
    // интенсивность, которую мы рассчитали ранее,
    // диффузный цвет, световой спад и теневое излучение, цвет света
    col.rgb = lightIntensity * s.Albedo * atten * _LightColor0.rgb;
    // в случае, если мы хотим сделать шейдер прозрачным в будущем
    // лучше это сделать прямо сейчас
    col.a = s.Alpha; 

    return col;
}

Это весь шейдер. Преимущество этого заключается в том, что теперь мы можем добавлять различные виды наклонов, включая наклоны с цветами. Например, при использовании этого наклона, который имеет теплую лицевую сторону и синеватую холодную заднюю сторону с преувеличенным переходом, я получил из примеров Unity https://docs.unity3d.com/Manual/SL-SurfaceShaderLightingExamples.html.

получим такую картину:

Одна вещь, которую мы не писали для нашего шейдера, это эмиссия. Поскольку излучение является это объект светильника, который излучает его независимо от других источников света,- он не рассчитывается в функции освещения.
Этот toon shader замечательный и гибкий, и я видел, как он использовался во многих местах.
Функции освещения в целом очень полезны и мощны. Единственное, что нужно иметь в виду, это то, что они работают только в прямом рендеринге (forward rendering). Когда вы переключаете режим рендеринга на отложенный (deferred), вы все равно можете видеть объекты, но они не могут воспользоваться преимуществами отложенного рендеринга (не беспокойтесь об этом и придерживайтесь прямого рендеринга, если вы не знаете разницы).
Shader "Tutorial/013_CustomSurfaceLighting" {
    // показываем значения для редактирования в инспекторе
    Properties {
        _Color ("Tint", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
        [HDR] _Emission ("Emission", color) = (0,0,0)

        _Ramp ("Toon Ramp", 2D) = "white" {}
    }
    SubShader {
        //материал полностью непрозрачный и рендерится с непрозрачными объектами
        Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

        CGPROGRAM

          // шейдер является поверхностным шейдером, что означает, что он будет расширен
          // на Unity в фоновом режиме, чтобы иметь хорошее освещение и другие функции
          // наша поверхностная шейдерная функция называется surf,
          // и мы используем нашу собственную модель освещения
        //fullforwardshadows гарантирует, что Unity добавит тени, которым может понадобиться шейдер
        #pragma surface surf Custom fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;
        fixed4 _Color;
        half3 _Emission;

        sampler2D _Ramp;

        //наша функция освещения. Будет вызываться один раз за светильник
        float4 LightingCustom(SurfaceOutput s, float3 lightDir, float atten){
            //насколько нормаль направлена на светильник
            float towardsLight = dot(s.Normal, lightDir);
            //преобразуем значение от -1 до 1 к от 0 до 1
            towardsLight = towardsLight * 0.5 + 0.5;

            //читаем текстуру наклона
            float3 lightIntensity = tex2D(_Ramp, towardsLight).rgb;

            //комбинируем цвета
            float4 col;
              // интенсивность, которую мы рассчитали ранее,
              // диффузный цвет, световой спад и теневое излучение, цвет света
            col.rgb = lightIntensity * s.Albedo * atten * _LightColor0.rgb; 
              // в случае, если мы хотим сделать шейдер прозрачным в будущем
              // лучше это сделать прямо сейчас
            col.a = s.Alpha; 

            return col;
        }

        // входная структура, которая автоматически заполняется Unity
        struct Input {
            float2 uv_MainTex;
        };

          // функция поверхностного шейдера, которая устанавливает параметры,
          // которые затем использует функция освещения
        void surf (Input i, inout SurfaceOutput o) {
            //бразец и оттенок альбедо текстуры
            fixed4 col = tex2D(_MainTex, i.uv_MainTex);
            col *= _Color;
            o.Albedo = col.rgb;

            //o.Emission = _Emission;
        }
        ENDCG
    }
    FallBack "Standard"
}
Надеюсь, я смог объяснить, как реализовать пользовательские функции освещения в поверхностных шейдерах.


Вы также можете найти исходный код для этого шейдера здесь: https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/013_CustomSurfaceLighting/CustomLighting.shader

Перевод Беляев В.А. ака seaman

Комментариев нет:

Отправить комментарий