Постобработка с текстурой глубины.

Введение.

В последнем уроке я объяснил, как делать очень простые эффекты постобработки. Одним из важных инструментов для создания более сложных эффектов является доступ к буферу глубины. Это текстура, в которой сохранено расстояние пикселей от камеры.
Перед этим уроком Вам нужно понять, как постобработка работает вообще в Unity. У меня есть урок по этому поводу.

Чтение глубины.

Мы начнем с файлов, которые мы сделали в простом уроке по постобработке.
Первое, что мы расширяем, - это скрипт C#, который вставляет наш материал в конвейер рендеринга. Мы будем расширять его, поэтому, когда он запустится, он будет искать камеру в том же игровом объекте, что он сам, и скажет ей, создать буфер глубины для использования. Это делается с помощью флагов режима глубины. Мы могли бы просто настроить его для отображения буфера глубины, но мы возьмем существующее значение и установим нужный бит. Таким образом, мы не перезаписываем другие флаги, которые другие скрипты могут использовать, чтобы сделать свои собственные эффекты (вы можете почитать про битмаски, если вам интересно, как это работает).
private void Start(){
    Camera cam = GetComponent();
    cam.depthTextureMode = cam.depthTextureMode | DepthTextureMode.Depth;
}
Это все, что нам нужно изменить на стороне C#, чтобы получить доступ к текстуре глубины, поэтому теперь мы можем начать писать наш шейдер.
Мы получаем доступ к текстуре глубины, создавая новый образец текстуры, который мы называем _CameraDepthTexture. Мы можем читать ее из sampler, как и любую другую текстуру, поэтому мы можем просто взять ее и посмотреть, как выглядит текстура глубины. Поскольку глубина - всего лишь одно значение, она сохраняется только в красном значении текстуры, а другие цветовые каналы пустые, поэтому мы просто берем красное значение.
//тестура глубины
sampler2D _CameraDepthTexture;
//фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
    //берем глубину из текстуры глубины
    float depth = tex2D(_CameraDepthTexture, i.uv).r;

    return depth;
}
После этого и запуска игры шансы высоки, что игра выглядит в основном черной. Это потому, что глубина не кодируется линейно, расстояния ближе к камере более точны, чем те, которые находятся дальше, потому что здесь требуется больше точности. Если мы поместим камеру очень близко к объектам, мы увидим более яркий цвет, указывая, что объект близок к камере. Если вы все еще видите черный / в основном черный, когда камера приближается к объектам, попробуйте увеличить ближнее расстояние отсечения у камеры.

Чтобы сделать это более удобным для себя, мы должны декодировать глубину. К счастью, Unity дает метод для нас, который берет глубину, как мы ее сейчас, и возвращает линейную глубину между 0 и 1, 0, находящуюся в камере, и 1 находится на дальней плоскости отсечения. Если ваше изображение в основном черное с белым skybox, вы можете попытаться уменьшить дальнюю плоскость отсечения вашей камеры, чтобы увидеть больше оттенков.
// фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
    // берем глубину из текстуры глубины
    float depth = tex2D(_CameraDepthTexture, i.uv).r;
    // линеаризируем глубину между камерой и дальней плоскостью отсечения
    depth = Linear01Depth(depth);

    return depth;
}
 

Следующий шаг - полностью отделить глубину от настроек камеры, чтобы мы могли изменять их, не изменяя результаты наших эффектов. Мы достигаем этого, просто умножая линейную глубину, которую мы имеем теперь на расстояние дальней плоскости отсечения. Ближайшие и дальние плоскости отсечения предоставляются нам Unity через переменную проектора, а плоскость отсечения находится в z-компоненте.
// фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
    // берем глубину из текстуры глубины
    float depth = tex2D(_CameraDepthTexture, i.uv).r;
    // линеаризируем глубину между камерой и дальней плоскостью отсечения
    depth = Linear01Depth(depth);
    //Глубина как расстояние от камеры
    depth = depth * _ProjectionParams.z;

    return depth;
}
 

Поскольку большинство объектов находятся дальше, чем 1 единица от камеры, изображение будет в основном белым теперь. Но теперь у нас есть значение, которое мы можем использовать, не зависящее от плоскостей отсечения камеры и в единицах измерения, которые мы можем понять (Unity единицы).

Генерируем волну.

Теперь я покажу вам, как использовать эту информацию для создания волнового эффекта, который, как бы проходит по всему миру от игрока. Мы сможем настроить расстояние волны от игрока в данный момент, длину «хвоста» волны и цвет волны. Итак, первый шаг, который мы делаем, - это добавляем эти переменные к свойствам и как переменные в наш шейдер. Мы используем атрибут заголовка здесь, чтобы инспектор выводил жирным шрифтом часть переменных для волны в инспекторе, это вообще не изменяет функциональность шейдера.
//показываем значения для редактирования в инспекторе
Properties{
    [HideInInspector]_MainTex ("Texture", 2D) = "white" {}
    [Header(Wave)]
    _WaveDistance ("Distance from player", float) = 10
    _WaveTrail ("Length of the trail", Range(0,5)) = 1
    _WaveColor ("Color", Color) = (1,0,0,1)
}
// переменные для управления волной
float _WaveDistance;
float _WaveTrail;
float4 _WaveColor;
 

Волна будет иметь жесткий разрез на переднем конце и гладкий хвост за этим. Мы начинаем с жесткого разреза, основанного на расстоянии. Для этого мы используем функцию шага, которая возвращает 0, если второе значение больше или 1 в противном случае.
    // расчет волны
    float waveFront = step(depth, _WaveDistance);

    return waveFront;
}
 

Затем для определения хвоста мы используем функцию smoothstep, которая аналогична функции step, за исключением того, что мы можем определить два значения для сравнения c третьим, если третье значение меньше первого, функция возвращает 0, если она больше, чем второй возвращает 1, другие значения возвращают значения между 0 и 1. Мне нравится представлять это как обратную линейную интерполяцию, потому что вы можете взять результат плавного перехода и поместить его в lerp с теми же минимальными и максимальными значениями, что и smoothstep чтобы получить значение третьего аргумента.
В этом случае значение, с которым мы хотим сравнить, - это глубина, наш максимум - это расстояние волны, а минимум - расстояние волны минус длина хвоста.
    float waveTrail = smoothstep(_WaveDistance - _WaveTrail, _WaveDistance, depth);
    return waveTrail;
}
 

Вы можете заметить, что фронт и след волны противоположны, было бы легко исправить это (перевернуть два аргумента клипа или перевернуть минимум и максимум плавного шага), но в этом случае это сделано специально. Т.к. если мы умножим любое число на ноль, оно станет равным нулю, теперь мы можем умножить фронт и след волны, и он станет равным нулю впереди и позади волны с небольшой волной в середине на нашем определенном расстоянии.
// расчет волны
    float waveFront = step(depth, _WaveDistance);
    float waveTrail = smoothstep(_WaveDistance - _WaveTrail, _WaveDistance, depth);
    float wave = waveFront * waveTrail;

    return wave;
}
 

Теперь, когда мы определили нашу волну, мы можем вернуть цвет изображению. Для этого сначала нужно получить исходное изображение, а затем сделать линейную интерполяцию из исходного изображения в наш волновой цвет на основе только что рассчитанного параметра волны.
// примешиваем волну в исходный цвет
fixed4 col = lerp(source, _WaveColor, wave);

return col;
 

Как вы можете видеть, у нас есть артефакт когда расстояние достигает плоскости отсечения. Несмотря на то, что skybox технически находится на расстоянии от плоскости отсечения, мы не хотим показывать волну, когда она достигает ее.
Чтобы исправить это, мы читаем исходный цвет сразу после того, как мы вычислим глубину и вернем ее мгновенно, если глубина находится на далекой плоскости отсечения.
// фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
    // получаем глубину из текстуры глубины
    float depth = tex2D(_CameraDepthTexture, i.uv).r;
    // линеаризируем глубину между камерой и дальней плоскостью отсечения
    depth = Linear01Depth(depth);
    // глубина как расстояние от камеры 
    depth = depth * _ProjectionParams.z;

    // получаем исходный цвет
    fixed4 source = tex2D(_MainTex, i.uv);
    // пропускаем волну и возвращаем исходный цвет, если мы на skybox
    if(depth >= _ProjectionParams.z)
        return source;

    // рассчитываем волну
    float waveFront = step(depth, _WaveDistance);
    float waveTrail = smoothstep(_WaveDistance - _WaveTrail, _WaveDistance, depth);
    float wave = waveFront * waveTrail;

    // примешиваем волну в цвет
    fixed4 col = lerp(source, _WaveColor, wave);

    return col;
}

Двигаем волну.

Последнее, что я хотел бы сделать, это расширить скрипт C#, чтобы автоматически установить расстояние до волны и заставить ее медленно уйти от игрока. Я бы хотел контролировать скорость движения волны, если волна активна. Также мы должны помнить текущее расстояние волны. Для всего этого мы добавляем несколько новых переменных класса в наш скрипт.
[SerializeField]
private Material postprocessMaterial;
[SerializeField]
private float waveSpeed;
[SerializeField]
private bool waveActive;
Затем мы добавляем метод Update, который автоматически вызывается Unity каждый кадр. В нем мы увеличиваем расстояние волны, если она активна, и устанавливаем его на ноль, когда это не так, таким образом, волна сбрасывается и исходит от игрока каждый раз, когда мы включаем ее снова.
private void Update(){
    //Если волна активна двигаем ее прочь
    if(waveActive){
        waveDistance = waveDistance + waveSpeed * Time.deltaTime;
    } else {
    // иначе обнуляем ее
        waveDistance = 0;
    }
}
И затем используем переменную wavedistance в нашем шейдере. Мы выполняем настройку в OnRenderImage непосредственно перед использованием метода, таким образом, мы можем убедиться, что когда он используется, он настроен на правильное значение.
// метод, который автоматически вызывает Unity после того как камера сделала рендеринг
private void OnRenderImage(RenderTexture source, RenderTexture destination){
    // синхронизируем расстояние для шейдера
    postprocessMaterial.SetFloat("_WaveDistance", waveDistance);
    // рисуем пиксели из текстуры-источника в текстуру-приемник
    Graphics.Blit(source, destination, postprocessMaterial);
}
 

Shader "Tutorial/017_Depth_Postprocessing"{
    // показываем значения для редактирования в инспекторе
    Properties{
        [HideInInspector]_MainTex ("Texture", 2D) = "white" {}
        [Header(Wave)]
        _WaveDistance ("Distance from player", float) = 10
        _WaveTrail ("Length of the trail", Range(0,5)) = 1
        _WaveColor ("Color", Color) = (1,0,0,1)
    }

    SubShader{
          // Маркер, который указывает, что нам не нужен клиппинг 
          // и чтение / запись буфера глубины
        Cull Off
        ZWrite Off 
        ZTest Always

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

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

            // текстура отрендеренного экрана
            sampler2D _MainTex;

            // текстура глубины
            sampler2D _CameraDepthTexture;

            // переменные для управления волной
            float _WaveDistance;
            float _WaveTrail;
            float4 _WaveColor;


            // объект, который приходит в вершинный шейдер
            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{
                // получаем глубину из текстуры глубины
                float depth = tex2D(_CameraDepthTexture, i.uv).r;
                // линеаризируем глубину между камерой и дальней плоскостью отсечения
                depth = Linear01Depth(depth);
                // глубина как расстояние от камеры
                depth = depth * _ProjectionParams.z;

                // получаем исходный цвет
                fixed4 source = tex2D(_MainTex, i.uv);
                // пропускаем волну и возвращаем исходный цвет, если мы на skybox
                if(depth >= _ProjectionParams.z)
                    return source;

                // рассчитываем волну
                float waveFront = step(depth, _WaveDistance);
                float waveTrail = smoothstep(_WaveDistance - _WaveTrail, _WaveDistance, depth);
                float wave = waveFront * waveTrail;

                // примешиваем волну в цвет
                fixed4 col = lerp(source, _WaveColor, wave);

                return col;
            }
            ENDCG
        }
    }
}
using UnityEngine;

// скрипт, висящий на главной камере
public class DepthPostprocessing : MonoBehaviour {
    // Материал, используемый для постобработки
    [SerializeField]
    private Material postprocessMaterial;
    [SerializeField]
    private float waveSpeed;
    [SerializeField]
    private bool waveActive;

    private float waveDistance;

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

    private void Update(){
        // Если волна активна двигаем ее прочь
        if(waveActive){
            waveDistance = waveDistance + waveSpeed * Time.deltaTime;
        } else { 
          // иначе обнуляем ее
            waveDistance = 0;
        }
    }

    // метод, который автоматически вызывает Unity после того как камера сделала рендеринг
    private void OnRenderImage(RenderTexture source, RenderTexture destination){
        // синхронизируем расстояние для шейдера
        postprocessMaterial.SetFloat("_WaveDistance", waveDistance);
        // рисуем пиксели из текстуры-источника в текстуру-приемник
        Graphics.Blit(source, destination, postprocessMaterial);
    }
}
Надеюсь, я смог объяснить, как использовать буфер глубины для эффектов постпроцессинга, и теперь вы сможете сделать свои собственные эффекты.



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

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

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