Постпроцессы с текстурой нормалей.

Введение.

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

Читаем глубину и нормаль.

Мы начинаем этот урок с файлов из предыдущего урока, и будем расширять их по мере необходимости.
Первое изменение - удалить весь код из скрипта C#, который мы использовали для перемещения волны. Затем мы не будем указывать камере, отображать глубину объектов, вместо этого мы сделаем так, чтобы она отображала текстуру, которая включает в себя как глубину, так и нормали.
private void Start(){
    //берем камеру и указываем ей визуализировать текстуру глубины/нормалей
    cam = GetComponent();
    cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.DepthNormals;
}
И это все настройки, которые нам нужны для доступа к нормалям. Затем мы редактируем шейдер.
Мы также удалим весь код, используемый для волновой функции. Затем мы переименуем _CameraDepthTexture в _CameraDepthNormalsTexture, чтобы она заполнялась Unity.
//Показываем значение в инспекторе
Properties{
    [HideInInspector]_MainTex ("Texture", 2D) = "white" {}
}
…
//текстура глубины/нормали
sampler2D _CameraDepthNormalsTexture;
С помощью этой настройки мы теперь можем читать текстуру глубины внутри нашего фрагментарного шейдера. Если мы просто сделаем это и просто нарисуем текстуру на экране, мы уже увидим что-то интересное.
//фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
    //читаем глубину/нормаль
    float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);

    return depthnormal;
}

Но то, что мы можем видеть, - это не то, что мы действительно хотим, мы видим только красные и зеленые значения и немного синего на расстоянии. Это потому, что, как видно из названия, эта текстура содержит нормали, а также текстуру глубины, поэтому мы должны сначала ее декодировать. К счастью, Unity дает нам метод, который делает именно это. Мы должны дать ей значение глубины/нормали, а также два других значения, в которые функция будет писать глубину и нормали.
В отличие от текстуры глубины, значение глубины, которое мы имеем сейчас, уже линейно между камерой и дальним планом, поэтому мы можем легко адаптировать код из предыдущего урока, чтобы снова получить расстояние от камеры.
// фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
    // читаем глубину/нормаль
    float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);

    //декодируем глубину/нормаль
    float3 normal;
    float depth;
    DecodeDepthNormal(depthnormal, depth, normal);

    //получаем глубину как дистанцию от камеры 
    depth = depth * _ProjectionParams.z;

    return depth;
}
Но вернемся к использованию нормалей. Когда мы просто выведем нормали как цвета на экране, мы получим довольно хороший результат.
// фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
    // читаем глубину/нормаль
    float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);

    // декодируем глубину/нормаль
    float3 normal;
    float depth;
    DecodeDepthNormal(depthnormal, depth, normal);

    // получаем глубину как дистанцию от камеры
    depth = depth * _ProjectionParams.z;

    return float4(normal, 1);
}
Но если мы поворачиваем камеру, мы можем видеть, что точка на поверхности не всегда имеет одни значения нормали, потому что нормали хранятся относительно камеры. Поэтому, если мы хотим получить нормаль в мире, мы должны пойти на дополнительные шаги.

В мировое пространство.

Мы можем легко преобразовать наши нормали из системы координат камеры в мировое пространство, но, к сожалению, Unity не дает нам функции для этого, поэтому мы должны сделать это сами в нашем шейдере. Итак, мы вернемся к нашему скрипту C# и передадим в шейдер нужные значения.
Сначала мы получаем ссылку на нашу камеру. Мы уже получили камеру в нашем методе Start, поэтому мы можем непосредственно сохранить ее в переменной класса. Затем в методе OnRenderImage мы получаем матрицу преобразования из пространства просмотра в мировое пространство, и передаем ее в наш шейдер. Причина, по которой мы не можем передать матрицу в наш шейдер один раз в методе Start, заключается в том, что мы хотим перемещать и поворачивать нашу камеру после запуска эффекта, и матрица изменяется, когда мы это делаем.
using UnityEngine;

//Скрипт лежит на объекте камеры
public class NormalPostprocessing : MonoBehaviour {
 //Материал, который применяется при постпроцессинге
 [SerializeField]
 private Material postprocessMaterial;

 private Camera cam;

 private void Start(){
  // берем камеру и указываем ей визуализировать текстуру глубины/нормалей
  cam = GetComponent();
  cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.DepthNormals;
 }

 //метод автоматически вызывается после того как камера орендерила
 private void OnRenderImage(RenderTexture source, RenderTexture destination){
  //Получаем матрицу преобразования и передаем ее в шейдер
  Matrix4x4 viewToWorld = cam.cameraToWorldMatrix;
  postprocessMaterial.SetMatrix("_viewToWorld", viewToWorld);
  //Рисуем пиксель из текстуры источника в текстуру приемник
  Graphics.Blit(source, destination, postprocessMaterial);
 }
}
Затем мы можем использовать эту матрицу в нашем шейдере. Мы добавляем для него новую переменную, а затем умножаем ее на нормаль перед ее использованием. Мы преобразуем ее в матрицу 3x3 перед умножением, поэтому изменение позиции не применяется, а только вращение. Это все, что нам нужно для нормалей.
//матрица для конвертирования из пространства просмотра в мировое пространство
float4x4 _viewToWorld;
    normal = normal = mul((float3x3)_viewToWorld, normal);
    return float4(normal, 1);
}

Цвет верха.

Теперь, когда у нас есть нормали мира, мы можем сделать простой эффект. Мы можем покрасить верхнюю часть всех объектов в сцене в определенный цвет.
Для этого мы просто сравниваем нормаль с вектором вверх. Мы делаем это через dot продукт, который возвращает 1, когда оба нормированных вектора указывают в одном направлении (когда поверхность плоская), 0, когда они ортогональны (в нашем случае на стенах) и -1, когда они противоположны каждому другой (в нашем случае это означает крышу над камерой).
    float up = dot(float3(0,1,0), normal);
    return up;
}
Чтобы сделать более очевидным то, что сверху, а что нет, теперь мы можем взять это плавное значение и использовать step, чтобы отличать верхнюю и верхнюю части. Если второе значение меньше, оно вернет 0, и мы увидим черное, если оно больше, мы увидим белый.
float up = dot(float3(0,1,0), normal);
up = step(0.5, up);
return up;
Следующий шаг - вернуть исходные цвета, где мы не определили верх поверхности. Для этого мы просто читаем основную текстуру, а затем делаем линейную интерполяцию между этим цветом и цветом, который мы определяем, сверху (белый на данный момент).
float up = dot(float3(0,1,0), normal);
up = step(0.5, up);
float4 source = tex2D(_MainTex, i.uv);
float4 col = lerp(source, float4(1,1,1,1), up);
return col;

Настраиваемость.

И в качестве последнего шага мы добавим некоторую настраиваемость. Поэтому мы добавляем свойство и глобальную переменную для значения обрезания направления вверх и для верхнего цвета.
_upCutoff ("up cutoff", Range(0,1)) = 0.7
_topColor ("top color", Color) = (1,1,1,1)
//настройка эффекта
float _upCutoff;
float4 _topColor;
Затем мы заменяем фиксированный 0,5, который мы использовали ранее для нашего значения отсечки с новой переменной отсечки, и линейно интерполируем на верхний цвет вместо белого цвета. Затем мы также можем умножить цвет вверх с альфа-значением верхнего цвета, таким образом, когда мы понижаем альфа-значение, верхняя часть пропускает часть исходного цвета.
    float up = dot(float3(0,1,0), normal);
    up = step(_upCutoff, up);
    float4 source = tex2D(_MainTex, i.uv);
    float4 col = lerp(source, _topColor, up * _topColor.a);
    return col;
}

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

Исходники.

using UnityEngine;

// Скрипт лежит на объекте камеры
public class NormalPostprocessing : MonoBehaviour {
    // Материал, который применяется при постпроцессинге
    [SerializeField]
    private Material postprocessMaterial;

    private Camera cam;

    private void Start(){
        // берем камеру и указываем ей визуализировать текстуру глубины/нормалей
        cam = GetComponent();
        cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.DepthNormals;
    }

    // метод автоматически вызывается после того как камера орендерила
    private void OnRenderImage(RenderTexture source, RenderTexture destination){
        // получаем матрицу преобразования и передаем ее в шейдер
        Matrix4x4 viewToWorld = cam.cameraToWorldMatrix;
        postprocessMaterial.SetMatrix("_viewToWorld", viewToWorld);
        //Рисуем пиксель из текстуры источника в текстуру приемник
        Graphics.Blit(source, destination, postprocessMaterial);
    }
}

Shader "Tutorial/018_Normal_Postprocessing"{
    //Показываем значения в инспекторе
    Properties{
        [HideInInspector]_MainTex ("Texture", 2D) = "white" {}
        _upCutoff ("up cutoff", Range(0,1)) = 0.7
        _topColor ("top color", Color) = (1,1,1,1)
    }

    SubShader{
        // Нам не нужен кулинг или запись/сравнение буфера глубины
        Cull Off
        ZWrite Off 
        ZTest Always

        Pass{
            CGPROGRAM
            //включаем полезные функции
            #include "UnityCG.cginc"

            //определяем вершинный и фрагментный шейдер
            #pragma vertex vert
            #pragma fragment frag

            // текстура отрендеренного экрана
            sampler2D _MainTex;
            //матрица для конвертирования из пространства камеры в мировое пространство
            float4x4 _viewToWorld;
            //текстура глубины/нормалей
            sampler2D _CameraDepthNormalsTexture;

            //настройка эффекта
            float _upCutoff;
            float4 _topColor;


            //the object data that's put into the vertex shader
            struct appdata{
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            //Данные для фрагментного шейдера
            struct v2f{
                float4 position : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            //вертексный шейдер
            v2f vert(appdata v){
                v2f o;
                //Конвертируем позицию из пространства объекта в пространство экрана
                o.position = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            //фрагментный шейдер
            fixed4 frag(v2f i) : SV_TARGET{
                //читаем глубину/нормаль
                float4 depthnormal = tex2D(_CameraDepthNormalsTexture, i.uv);

                //декодируем глубину/нормаль
                float3 normal;
                float depth;
                DecodeDepthNormal(depthnormal, depth, normal);

                //получаем глубину как дистанцию от камеры
                depth = depth * _ProjectionParams.z;

                normal = mul((float3x3)_viewToWorld, normal);

                float up = dot(float3(0,1,0), normal);
                up = step(_upCutoff, up);
                float4 source = tex2D(_MainTex, i.uv);
                float4 col = lerp(source, _topColor, up * _topColor.a);
                return col;
            }
            ENDCG
        }
    }
}
Надеюсь, я смог передать, как получить доступ к обычным текстурам и это станет прочной основой для будущих эффектов.



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

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

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