Контуры корпуса.

Введение.

До этого наносили цвет на экран один раз за шейдер (т.е. в шейдере был один проход), или позволяли Unity генерировать несколько проходов для нас при использовании поверхностных шейдеров. Но у нас есть возможность рисовать нашу сетку несколько раз в одном шейдере. Отличный способ использовать это - рисовать контуры. Сначала мы рисуем наш объект, как обычно, и затем рисуем его снова, но мы немного меняем вершины, поэтому он видим только вокруг исходного объекта, рисуя контур.
Чтобы понять этот урок, лучше всего сначала изучить поверхностные шейдеры.
Первая версия этого шейдера будет основана на простом текстурированном unlit шейдере.

Контур для Unlit шейдера.

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

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

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

    //текстура и трансформ текстуры
    sampler2D _MainTex;
    float4 _MainTex_ST;

    //оттенок текстуры
    fixed4 _Color;

    // данные передаваемые в вершинный шейдер
    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 = TRANSFORM_TEX(v.uv, _MainTex);
        return o;
    }

    //фрагментный шейдер
    fixed4 frag(v2f i) : SV_TARGET{
        fixed4 col = tex2D(_MainTex, i.uv);
        col *= _Color;
        return col;
    }

    ENDCG
}
Следующее изменение заключается в настройке наших свойств и переменных. Этот второй проход будет писать простой цвет на экран, поэтому нам не нужна текстура, нам просто нужен цвет контура и толщина контура. Мы помещаем свойства в область свойств наверху, как обычно. Важно, однако, что мы добавили новые переменные во второй проход.
//показываем значения для редактирования в инспекторе
Properties{
    _OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
    _OutlineThickness ("Outline Thickness", Range(0,.1)) = 0.03

    _Color ("Tint", Color) = (0, 0, 0, 1)
    _MainTex ("Texture", 2D) = "white" {}
}
//цвет контура
fixed4 _OutlineColor;
//толщина контура
float _OutlineThickness;
Следующий шаг - переписать наш шейдер фрагмента, чтобы использовать новую переменную вместо текстуры. Мы можем просто вернуть цвет без каких-либо дополнительных вычислений.
// фрагментный шейдер
fixed4 frag(v2f i) : SV_TARGET{
    return _OutlineColor;
}
Поскольку мы не читаем текстуру в этом проходе, мы также можем игнорировать координаты uv, поэтому мы удаляем их из нашей входной структуры, нашей структуры передаваемой в фрагментную функцию и останавливаем передачу между структурами в вершинном шейдере.
// данные передаваемые в вершинный шейдер
struct appdata{
    float4 vertex : POSITION;
};

// Данные передаваемые в фрагментный шейдер struct v2f{
    float4 position : SV_POSITION;
};

// вершинный шейдер
v2f vert(appdata v){
    v2f o;
    // конвертируем позицию вершины из пространства объекта в пространство экрана
    o.position = UnityObjectToClipPos(position);
    return o;
}
Теперь мы можем видеть в редакторе, что объекты теперь просто имеют цвет, который должны иметь контуры. Это потому, что наш второй проход просто зарисовывает все, что нарисовал первый проход. Мы это исправим позже.
Нам нужно гарантировать, что контуры фактически находятся за пределами базового объекта. Для этого мы просто расширяем их по своим нормалям. Это означает, что нам нужны нормали в нашей входной структуре, тогда мы просто добавляем их в положение вершин. Мы также нормализуем нормали и умножаем их на толщину контура, чтобы сделать контуры настолько толстыми, насколько мы хотим.
// данные передаваемые в вершинный шейдер 
struct appdata{
    float4 vertex : POSITION;
    float3 normal : NORMAL;
};
// вершинный шейдер
v2f vert(appdata v){
    v2f o;
    //рассчитываем позицию расширенного объекта
    float3 normal = normalize(v.normal);
    float3 outlineOffset = normal * _OutlineThickness;
    float3 position = v.vertex + outlineOffset;
    // конвертируем позицию вершины из пространства объекта в пространство экрана
    o.position = UnityObjectToClipPos(position);

    return o;
}
 
Теперь можем настроить толщину нашего корпуса, но он все еще скрывает базовые объекты. Чтобы исправить это - мы не должны рисовать переднюю часть корпуса. Обычно, когда мы визуализируем объекты, мы просто рисуем фронт из-за соображений производительности (вы могли заглянуть внутрь объекта раньше и смотреть наружу). Теперь мы можем инвертировать это и рисовать только обратную сторону. Это означает, что мы все еще можем видеть объект, потому что мы можем заглянуть в корпус, и мы можем видеть корпус позади объекта, потому что он больше самого объекта.
Чтобы сообщить Unity, чтобы не отображать передние стороны объектов, мы добавляем атрибут Cull Front к проходу корпуса за пределами области hlsl.
//Второй проход, где мы визуализируем контур
Pass{
    Cull Front

Теперь мы имеем контуры, как мы хотели.

Исходники.

Shader "Tutorial/19_InvertedHull/Unlit"{
    //показываем значения для редактирования в инспекторе
    Properties{
        _OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
        _OutlineThickness ("Outline Thickness", Range(0,.1)) = 0.03

        _Color ("Tint", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
    }

    SubShader{
        //Материал непрозрачный и рисуется с непрозрачными
        Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

        //первый проход, где мы рисуем сам объект
        Pass{
            CGPROGRAM

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

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

            //текстура и трансформ текстуры
            sampler2D _MainTex;
            float4 _MainTex_ST;

            //оттенок текстуры
            fixed4 _Color;

            //данные передаваемые в вершинный шейдер
            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 = TRANSFORM_TEX(v.uv, _MainTex);
                return o;
            }

            //фрагментный шейдер
            fixed4 frag(v2f i) : SV_TARGET{
                fixed4 col = tex2D(_MainTex, i.uv);
                col *= _Color;
                return col;
            }

            ENDCG
        }

        //второй проход, где мы рисуем контур
        Pass{
            Cull front

            CGPROGRAM

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

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

            // цвет контура
            fixed4 _OutlineColor;
            // толщина контура
            float _OutlineThickness;

            // данные передаваемые в вершинный шейдер
            struct appdata{
                float4 vertex : POSITION;
                float3 normal : NORMAL;
            };

            // Данные передаваемые в фрагментный шейдер
            struct v2f{
                float4 position : SV_POSITION;
            };

            // вершинный шейдер
            v2f vert(appdata v){
                v2f o;
                // рассчитываем позицию расширенного объекта
                float3 normal = normalize(v.normal);
                float3 outlineOffset = normal * _OutlineThickness;
                float3 position = v.vertex + outlineOffset;
                // конвертируем позицию вершины из пространства объекта в пространство экрана
                o.position = UnityObjectToClipPos(position);

                return o;
            }

            // фрагментный шейдер
            fixed4 frag(v2f i) : SV_TARGET{
                return _OutlineColor;
            }

            ENDCG
        }
    }

    //фолбек добавляет тени
    FallBack "Standard"
}

Контур с поверхностным шейдером.

Очень просто также применить контуры к поверхностному шейдеру. Unity создает для нас проходы поверхностного шейдера, но мы все равно можем использовать и свои собственные проходы, которых Unity не коснется, поэтому они работают как обычно.
Это означает, что мы можем просто скопировать контур из нашего unlit шейдера в поверхностный шейдер и заставить его работать так, как мы ожидаем.

Исходник.

Shader "Tutorial/020_InvertedHull/Surface" {
    Properties {
        _Color ("Tint", Color) = (0, 0, 0, 1)
        _MainTex ("Texture", 2D) = "white" {}
        _Smoothness ("Smoothness", Range(0, 1)) = 0
        _Metallic ("Metalness", Range(0, 1)) = 0
        [HDR] _Emission ("Emission", color) = (0,0,0)

        _OutlineColor ("Outline Color", Color) = (0, 0, 0, 1)
        _OutlineThickness ("Outline Thickness", Range(0,1)) = 0.1
    }
    SubShader {
        // Материал непрозрачный и рисуется с непрозрачными
        Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

        CGPROGRAM
        //the shader is a surface shader, meaning that it will be extended by unity in the background 
        //to have fancy lighting and other features
        //our surface shader function is called surf and we use our custom lighting model
        //fullforwardshadows makes sure unity adds the shadow passes the shader might need
        //vertex:vert makes the shader use vert as a vertex shader function
        #pragma surface surf Standard fullforwardshadows
        #pragma target 3.0

        sampler2D _MainTex;
        fixed4 _Color;

        half _Smoothness;
        half _Metallic;
        half3 _Emission;

        // данные передаваемые в вершинный шейдер
        struct Input {
            float2 uv_MainTex;
        };

        // функция поверхностного шейдера
        void surf (Input i, inout SurfaceOutputStandard o) {
            // читаем цвет альбедо из текстуры и применяем оттенок
            fixed4 col = tex2D(_MainTex, i.uv_MainTex);
            col *= _Color;
            o.Albedo = col.rgb;
            //просто применяем значения металличности, сглаженности и излучения
            o.Metallic = _Metallic;
            o.Smoothness = _Smoothness;
            o.Emission = _Emission;
        }
        ENDCG

        //Второй проход, где мы рисуем контур
        Pass{
            Cull Front

            CGPROGRAM

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

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

            // цвет контура
            fixed4 _OutlineColor;
               // толщина контура
            float _OutlineThickness;

            // данные передаваемые в вершинный шейдер
            struct appdata{
                float4 vertex : POSITION;
                float4 normal : NORMAL;
            };

            // Данные передаваемые в фрагментный шейдер
            struct v2f{
                float4 position : SV_POSITION;
            };

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

            // фрагментный шейдер
            fixed4 frag(v2f i) : SV_TARGET{
                return _OutlineColor;
            }

            ENDCG
        }
    }
    FallBack "Standard"
}
Различия контуров с помощью инвертированного шейдера корпуса от эффекта постпроцессинга заключаются в том, что вы можете создавать контуры на основе материала, вам не нужно применять его ко всем объектам. Также это выглядит иначе, чем выбор контуров на основе глубины и нормалей. Лучше всего знать об этих методах, а затем выбрать, что лучше для вашей игры.
Надеюсь, теперь ясно, как работают шейдеры с несколькими проходами и как их использовать для создания контуров.



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

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

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