Смещение по синусоиде.

Введение.

До сих пор мы использовали вершинный шейдер, чтобы переместить вершины из координат объекта в координаты экрана (или в мировые координаты, когда мы их использовали для других вещей). Но с вершинными шейдерами мы можем многое сделать. В качестве введения я покажу вам, как применять простую синусоидальную волну к модели, делая ее колеблющейся.
Я сделаю шейдер с поверхностным шейдером, так что Вы должны знать основы поверхностных шейдеров, но он работает одинаково с любым другим типом шейдеров.

Вершинная функция.

При манипулировании положениями нашей поверхности мы используем вершинный шейдер. До сих пор мы не писали вершинный шейдер в нашем поверхностном шейдере, а вместо его нам генерировал Unity в фоновом режиме. Чтобы изменить это, мы добавляем объявление для него в наше определение шейдера поверхности, добавляя часть vertex: vertexShaderName.

//шейдер поверхностный, т.е. он будет расширен автоматом Unity 
//чтобы получить хорошее освещение и другие функции
//Наш поверхностный шейдер называется surf.
//Также мы можем использовать пользовательское освещение
//fullforwardshadows заставляет Unity добавить просчет теней
//vertex:vert заставляет использовать функцию vert как функцию вершинного шейдера
#pragma surface surf Standard fullforwardshadows vertex:vert
Теперь мы должны написать саму вершинную функцию. Раньше в ней в unlit шейдерах мы вычисляли положение вершины в пространстве экрана, но при использовании поверхностных шейдеров это преобразование генерируется для нас Unity. Мы манипулируем позициями вершин пространства объектов, а затем позволяем им обрабатываться в Unity.
Поскольку входная структура должна иметь переменные с определенными именами, проще всего использовать входную структуру Unity для нас здесь, которая называется appadata_full, но мы могли бы также использовать нашу собственную структуру, если она использует ту же терминологию.
Как и поверхностный шейдер, вершинный шейдер в поверхностных шейдерах (для этого должна быть лучшая терминология) ничего не возвращает, вместо этого он принимает параметр с ключевым словом inout, с которым мы можем манипулировать.
Поскольку поверхностные шейдеры генерируют преобразование в пространство клипов для нас, пустая функция вершин - все, что нам нужно, чтобы заставить наш шейдер работать так же, как и раньше.

void vert(inout appdata_full data){

}
Простая вещь, которую мы можем сделать для нашей сетки, умножить позиции всех наших вершин на значение, чтобы сделать модель более крупной. (a * = b то же, что a = a * b, но немного короче)
void vert(inout appdata_full data){
    data.vertex.xyz *= 2;
}
Модель больше, но мы также видим странный артефакт. Тень все еще вычисляется на основе исходных, немодифицированных позиций вершин. Это связано с тем, что поверхностный шейдер автоматически не генерирует теневой проход (используемый для отбрасывания теней) для наших новых вершинных позиций. Чтобы исправить это, мы расширяем наше определение поверхности с помощью подсказок addshadows, и артефакты должны исчезнуть.

//addshadows говорит, что поверхностному шейдеру генерировать новый теневой проход,
// основанный на вершинном шейдере
#pragma surface surf Standard fullforwardshadows vertex:vert addshadow
Чтобы сделать шейдер более интересным, мы изменим шейдер вершин. Вместо того чтобы сделать модель более крупной, мы сместим позицию y, основываясь на синусе позиции x, делая ее волнистой.

void vert(inout appdata_full data){
    data.vertex.y += sin(data.vertex.x);
}
Это приводит к большим волнам с низкой частотой, поэтому мы добавим две переменные для изменения этих свойств.

//...

_Amplitude ("Wave Size", Range(0,1)) = 0.4
_Frequency ("Wave Freqency", Range(1, 8)) = 2

//...

float _Amplitude;
float _Frequency;

//...

void vert(inout appdata_full data){
float4 modifiedPos = data.vertex;
modifiedPos.y += sin(data.vertex.x * _Frequency) * _Amplitude;
data.vertex = modifiedPos;

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

Коррекция нормалей.

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

float3 posPlusTangent = data.vertex + data.tangent * 0.01;
posPlusTangent.y += sin(posPlusTangent.x * _Frequency) * _Amplitude;

float3 bitangent = cross(data.normal, data.tangent);
float3 posPlusBitangent = data.vertex + bitangent * 0.01;
posPlusBitangent.y += sin(posPlusBitangent.x * _Frequency) * _Amplitude;
С этими позициями мы можем теперь вычислить новую нормаль к поверхности. Для этого мы вычисляем новую касательную и бинормаль из позиций путем вычитания измененного положения базовой поверхности из модифицированных положений поверхности, где мы ранее добавляли касательную / бинормаль. И после получения новой касательной и бинормали, мы можем взять их кросс-продукт, чтобы получить новую нормаль, которую мы и используем.

void vert(inout appdata_full data){
    float4 modifiedPos = data.vertex;
    modifiedPos.y += sin(data.vertex.x * _Frequency) * _Amplitude;
    
    float3 posPlusTangent = data.vertex + data.tangent * 0.01;
    posPlusTangent.y += sin(posPlusTangent.x * _Frequency) * _Amplitude;

    float3 bitangent = cross(data.normal, data.tangent);
    float3 posPlusBitangent = data.vertex + bitangent * 0.01;
    posPlusBitangent.y += sin(posPlusBitangent.x * _Frequency) * _Amplitude;

    float3 modifiedTangent = posPlusTangent - modifiedPos;
    float3 modifiedBitangent = posPlusBitangent - modifiedPos;

    float3 modifiedNormal = cross(modifiedTangent, modifiedBitangent);
    data.normal = normalize(modifiedNormal);
    data.vertex = modifiedPos;
}

Добавляем время.

Последнее, что я хотел бы добавить к этому шейдеру, - это движение со временем. Пока мы используем только x-положение вершины как изменяющийся параметр в нашей функции, который генерирует новые позиции вершин, но добавление времени к этому довольно просто.
Unity передает время всем шейдерам автоматически как 4-мерный вектор, первый компонент вектора - это время, деленное на 20, второе - только время в секундах, третье - умноженное на 2, а четвертое - умноженное на время на 3. Поскольку мы хотим отрегулировать время через внешнее свойство, мы используем второй компонент с временем в секундах. Затем мы добавляем время, умноженное на скорость анимации, на позицию x.

_AnimationSpeed ("Animation Speed", Range(0,5)) = 1

//...

float _AnimationSpeed;

//...

void vert(inout appdata_full data){
    float4 modifiedPos = data.vertex;
    modifiedPos.y += sin(data.vertex.x * _Frequency + _Time.y * _AnimationSpeed) * _Amplitude;
    
    float3 posPlusTangent = data.vertex + data.tangent * 0.01;
    posPlusTangent.y += sin(posPlusTangent.x * _Frequency + _Time.y * _AnimationSpeed)
                       * _Amplitude;

    float3 bitangent = cross(data.normal, data.tangent);
    float3 posPlusBitangent = data.vertex + bitangent * 0.01;
    posPlusBitangent.y += sin(posPlusBitangent.x * _Frequency + _Time.y * _AnimationSpeed)
                       * _Amplitude;

    float3 modifiedTangent = posPlusTangent - modifiedPos;
    float3 modifiedBitangent = posPlusBitangent - modifiedPos;

    float3 modifiedNormal = cross(modifiedTangent, modifiedBitangent);
    data.normal = normalize(modifiedNormal);
    data.vertex = modifiedPos;
}
Я немного увеличил смещение позиций поверхности (до 0,01 единицы), чтобы лучше сгладить артефакты. Небольшое расстояние может представлять более сложные искажения лучше, в то время как большие расстояния разглаживаются над некоторыми вещами.

Shader "Tutorial/015_vertex_manipulation" {
    //Определяем свойства для редактирования в инспекторе
    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)

        _Amplitude ("Wave Size", Range(0,1)) = 0.4
        _Frequency ("Wave Freqency", Range(1, 8)) = 2
        _AnimationSpeed ("Animation Speed", Range(0,5)) = 1
    }
    SubShader {
        // материал полностью непрозрачен и визуализируется с непрозрачными
        Tags{ "RenderType"="Opaque" "Queue"="Geometry"}

        CGPROGRAM

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

        sampler2D _MainTex;
        fixed4 _Color;

        half _Smoothness;
        half _Metallic;
        half3 _Emission;

        float _Amplitude;
        float _Frequency;
        float _AnimationSpeed;

        //input struct which is automatically filled by unity
        struct Input {
            float2 uv_MainTex;
        };

        void vert(inout appdata_full data){
            float4 modifiedPos = data.vertex;
            modifiedPos.y += sin(data.vertex.x * _Frequency + _Time.y * _AnimationSpeed) * _Amplitude;
            
            float3 posPlusTangent = data.vertex + data.tangent * 0.01;
            posPlusTangent.y += sin(posPlusTangent.x * _Frequency + _Time.y * _AnimationSpeed)
                             * _Amplitude;

            float3 bitangent = cross(data.normal, data.tangent);
            float3 posPlusBitangent = data.vertex + bitangent * 0.01;
            posPlusBitangent.y += sin(posPlusBitangent.x * _Frequency + _Time.y * _AnimationSpeed)
                               * _Amplitude;

            float3 modifiedTangent = posPlusTangent - modifiedPos;
            float3 modifiedBitangent = posPlusBitangent - modifiedPos;

            float3 modifiedNormal = cross(modifiedTangent, modifiedBitangent);
            data.normal = normalize(modifiedNormal);
            data.vertex = modifiedPos;
        }

        // функция поверхностного шейдера, которая устанавливает параметры,
        // которые затем использует функция освещения
        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
    }
    FallBack "Standard"
}
Надеюсь, я смог объяснить, как начать манипулировать вершинами, и вы найдете свои собственные способы создания привлекательных шейдеров с помощью этой техники.


Вы также можете найти исходный код этого учебника здесь: https://github.com/ronja-tutorials/ShaderTutorials/blob/master/Assets/015_VertexManipulation/vertexmanipulation.shader

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

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

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