Shader — это не магия. Написание шейдеров в Unity. Вертексные шейдеры

    Всем привет! Меня зовут Дядиченко Григорий, и я основатель и CTO студии Foxsys. Сегодня мы поговорим про вершинные шейдеры. В статье будет разбираться практика с точки зрения Unity, очень простые примеры, а также приведено множество ссылок для изучения информации про шейдеры в Unity. Если вы разбираетесь в написании шейдеров, то вы не найдёте для себя ничего нового. Всем же кто хочет начать писать шейдеры в Unity, добро пожаловать под кат.



    Немного теории



    Для большего понимания процесса работы шейдера разберём совсем немного теории. Вертексный шейдер или вершинный шейдер — это программируемый этап работы шейдера, который работает с отдельными вертексами. Верктексы в свою очередь хранят в себе различные аттрибуты, которые обрабатываются данной частью шейдера, чтобы на выходе получить преобразованные аттрибуты.

    Примеры где используются вершинные шейдеры





    Деформация объектов — реалистичные волны, эффект ряби от дождя, деформация при попадании пули, всё это можно сделать вершинными шейдерами, и это будет выглядеть реалистичнее, чем тоже самое сделанное через Bump Mapping в фрагментной части шейдера. Так как это изменение геометрии. В шейдерах уровня 3.0 на эту тему есть техника под названием Dispacement Mapping, так как в них появился доступ к текстурам в вершинной части шейдера.



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



    Мультяшное освещение или стилизованное. Во многих играх с точки зрения стиля значительно интереснее выглядит не pbr освещение, а стилизация. При этом не имеет смысла рассчитывать ничего в фрагментной части.



    Скининг. В данный момент времени в игровых движках эта задача решена, но тем не менее в вертексных шейдерах полезно разбираться, чтобы понимать, как оно работает.

    Простые примеры работы с вертексами




    Не хочется чтобы получилось, как в старых уроках по рисованию совы, поэтому пойдём последовательно по этапам. Создадим стандартный surface шейдер. Это можно сделать по правой кнопке мыши в Project View или в верхней панели во вкладке Assets. Create->Shader->Standard Surface Shader.

    И получим такую стандартную заготовку.

    Surface Shader
    Shader "Custom/SimpleVertexExtrusionShader"
    {
    Properties
    {
    _Color ("Color", Color) = (1,1,1,1)
    _MainTex ("Albedo (RGB)", 2D) = "white" {}
    _Glossiness ("Smoothness", Range(0,1)) = 0.5
    _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
    Tags { "RenderType"="Opaque" }
    LOD 200

    CGPROGRAM
    // Physically based Standard lighting model, and enable shadows on all light types
    #pragma surface surf Standard fullforwardshadows

    // Use shader model 3.0 target, to get nicer looking lighting
    #pragma target 3.0

    sampler2D _MainTex;

    struct Input
    {
    float2 uv_MainTex;
    };

    half _Glossiness;
    half _Metallic;
    fixed4 _Color;

    // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
    // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
    // #pragma instancing_options assumeuniformscaling
    UNITY_INSTANCING_BUFFER_START(Props)
    // put more per-instance properties here
    UNITY_INSTANCING_BUFFER_END(Props)

    void surf (Input IN, inout SurfaceOutputStandard o)
    {
    // Albedo comes from a texture tinted by color
    fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
    o.Albedo = c.rgb;
    // Metallic and smoothness come from slider variables
    o.Metallic = _Metallic;
    o.Smoothness = _Glossiness;
    o.Alpha = c.a;
    }
    ENDCG
    }
    FallBack "Diffuse"
    }

    Как в ней что работает и в целом подробно мы её разберём в статье после базовой практики, плюс частично будем разбираться по ходу реализации шейдеров. Пока пусть часть вещей останутся, как данность. Если коротко, тут нет никакой магии (в плане того, как подцепляются параметры и прочее) Просто по определённым ключевым словам юнити генерирует код за вас, чтобы не писать его с нуля. Поэтому этот процесс достаточно не очевиден. Подробнее про surface шейдер и его свойства в Unity можно прочитать тут. docs.unity3d.com/Manual/SL-SurfaceShaders.html

    Удалим из него всё лишнее, чтобы оно не отвлекало, так как в данный момент времени оно не нужно. И получим такой короткий шейдер.

    Упрощённый шейдер
    Shader "Custom/SimpleVertexExtrusionShader"
    {
    Properties
    {
    _Color ("Color", Color) = (1,1,1,1)
    }
    SubShader
    {
    Tags { "RenderType"="Opaque" }
    LOD 200

    CGPROGRAM

    #pragma surface surf Standard fullforwardshadows

    #pragma target 3.0

    struct Input
    {
    float4 color : COLOR;
    };

    fixed4 _Color;

    void surf (Input IN, inout SurfaceOutputStandard o)
    {
    fixed4 c = _Color;
    o.Albedo = c.rgb;
    }
    ENDCG
    }
    FallBack "Diffuse"
    }




    Просто цвет на модели с освещением. За расчёт освещения в данном случае отвечает Unity.

    Для начала добавим самый простой эффект из примеров Unity. Экструзия по нормали, и на его примере разберём, как это работает.

    Для этого добавим в строчку #pragma surface surf Standard fullforwardshadows модификатор vertex:vert. Если в качестве параметра функции мы передаём inout appdata_full v то в сущности эта функция является модификатором вертексов. По своей сути — это часть вертексного шейдера, который создан кодогенерацией юнити, которая осуществляет предварительную обработку вертексов. Так же в блок Properties добавим поле _Amount принимающее значения от 0 до 1. Для использования поля _Amount в шейдере, нам так же нужно определить его там. В функции мы будем просто сдвигать на нормаль в зависимости от _Amount, где 0 — это стандартная позиция вертекса (нулевой сдвиг), а 1 — сдвиг ровно на нормаль.

    SimpleVertexExtrusionShader
    Shader "Custom/SimpleVertexExtrusionShader"
    {
    Properties
    {
    _Color ("Color", Color) = (1,1,1,1)
    _Amount ("Extrusion Amount", Range(0,1)) = 0.5
    }
    SubShader
    {
    Tags { "RenderType"="Opaque" }
    LOD 200

    CGPROGRAM

    #pragma surface surf Standard fullforwardshadows vertex:vert

    #pragma target 3.0

    struct Input
    {
    float4 color : COLOR;
    };

    fixed4 _Color;
    float _Amount;

    void vert (inout appdata_full v)
    {
    v.vertex.xyz += v.normal * _Amount;
    }
    void surf (Input IN, inout SurfaceOutputStandard o)
    {
    fixed4 c = _Color;
    o.Albedo = c.rgb;
    }
    ENDCG
    }
    FallBack "Diffuse"
    }


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

    Например один из самых простых способов сделать анимацию, использовать время для изменения амплитуды. В юнити есть встроенные переменные полный список которых можно найти тут docs.unity3d.com/Manual/SL-UnityShaderVariables.html В данном случае напишем новый шейдер на основе нашего прошлого шейдера. Вместо _Amount — сделаем float значение _Amplitude и воспользуемся встроенной переменной Unity _SinTime. _SinTime — это синус времени, и поэтому он принимает значения от -1 до 1. При этом не стоит забывать, что все встроенные переменные времени в шейдерах юнити являются векторами float4. Например _SinTime определяется как (sin(t/8), sin(t/4), sin(t/2), sin(t)), где t — это время. Поэтому возьмём компоненту z, чтобы анимация была побыстрее. И получим:

    SimpleVertexExtrusionWithTime
    Shader "Custom/SimpleVertexExtrusionWithTime"
    {
    Properties
    {
    _Color ("Color", Color) = (1,1,1,1)
    _Amplitude ("Extrusion Amplitude", float) = 1
    }
    SubShader
    {
    Tags { "RenderType"="Opaque" }
    LOD 200

    CGPROGRAM

    #pragma surface surf Standard fullforwardshadows vertex:vert

    #pragma target 3.0

    struct Input
    {
    float4 color : COLOR;
    };

    fixed4 _Color;
    float _Amplitude;

    void vert (inout appdata_full v)
    {
    v.vertex.xyz += v.normal * _Amplitude * (1 - _SinTime.z);
    }
    void surf (Input IN, inout SurfaceOutputStandard o)
    {
    fixed4 c = _Color;
    o.Albedo = c.rgb;
    }
    ENDCG
    }
    FallBack "Diffuse"
    }




    Итак, это были простые примеры. Пора рисовать сову!

    Деформация объектов





    На тему одного эффекта деформации у меня уже написана целая статья с подробным разбором математики процесса и логики мышления при разработке подобного эффекта habr.com/ru/post/435828 Это и будет нашей совой.

    Все шейдеры в статье написаны на hlsl. У этого языка на самом деле есть своя объёмная документация, о которой многие забывают и удивляются откуда берётся половина зашитых функций, хотя они определены в HLSL docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-intrinsic-functions

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

    Низкоуровневые шейдеры





    По старой доброй традиции работы с шейдерами здесь и в дальнейшем будем мучать стенфордского кролика.

    В целом так называемый ShaderLab в юнити — это по сути визуализация инспектора с полями в материалах и некоторое упрощение написания шейдеров.

    Возьмём общую структуру Shaderlab шейдера:

    Общая структура шейдера
    Shader "MyShaderName"
    {
    Properties
    {
    // свойства материала
    }
    SubShader // сабшейдер для определённого железа (можно определить директивой компиляции)
    {
    Pass
    {
    // проход шейдера
    }
    // для некоторых эффектов может понадобится несколько проходов
    }
    // может понадобится больше сабшейдеров
    FallBack "VertexLit" // в случае если на определённом железе не работает шейдер, то к каком откатиться
    }


    Такие директивы компиляции как
    #pragma vertex vert
    #pragma fragment frag
    определяют какие функции шейдера компилировать в качестве вершинного и фрагментного шейдера соответственно.

    Скажем возьмём один из самых частых примеров — шейдер для вывода цвета нормалей:

    SimpleNormalVisualization
    Shader "Custom/SimpleNormalVisualization"
    {
    Properties
    {
    }
    SubShader
    {
    Pass
    {
    CGPROGRAM

    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

    struct v2f {
    float4 pos : SV_POSITION;
    fixed3 color : COLOR0;
    };

    v2f vert (appdata_base v)
    {
    v2f o;
    o.pos = UnityObjectToClipPos(v.vertex);
    o.color = v.normal * 0.5 + 0.5;
    return o;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    return fixed4 (i.color, 1);
    }
    ENDCG
    }
    }
    FallBack "VertexLit"
    }




    В данном случае в вертексной части мы записываем в цвет вершины преобразованное значение нормали, а в пиксельной части применяем этот цвет в качестве цвета модели.

    Функция UnityObjectToClipPos — это вспомогательная функция Unity (из файла UnityCG.cginc), которая переводит вертексы объекта в позицию связанную с камерой. Без неё объект, при попадании в зону видимости (фруструм) камеры, будет рисоваться в координатах экрана вне зависимости от положения трансформа. Так как первоначально позиции вертексов представлены в координатах объекта. Просто значения относительно его пивота.

    Этот блок.
    struct v2f {
    float4 pos : SV_POSITION;
    fixed3 color : COLOR0;
    };

    Это определение структуры, которая будет обрабатываться в вертексной части и передаваться в фрагментную. В данном случае в ней определено, чтобы из меша забиралось два параметра — позиция вершины и цвет вершины. Подробнее про то, какие данные возможно прокидывать в юнити, можно прочитать по этой ссылке docs.unity3d.com/Manual/SL-VertexProgramInputs.html

    Важное уточнение. Названия аттрибутов меша не играют никакой роли. То есть допустим в аттрибут color вы можете писать вектор отклонения от первоначальной позиции (таким образом иногда делают эффект, когда идёт персонаж, чтобы трава от него «отталкивалась»). То как будет обрабатываться данный аттрибут целиком и полностью зависит от вашего шейдера.

    Заключение



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

    В будущих статьях мы разберём остальные виды шейдеров, отдельные эффекты, и я постараюсь описать свою логику мышления при создании новых или сложных эффетков.

    Так же создан репозиторий, куда будут складываться все результаты этого цикла статей github.com/Nox7atra/ShaderExamples Надеюсь эта информация будет полезна новичкам, которые только начинают свой путь в изучении этой темы.

    Немного полезных ссылок (и источников в том числе):


    www.khronos.org/opengl/wiki/Vertex_Shader
    docs.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-reference
    docs.unity3d.com/ru/current/Manual/SL-Reference.html
    docs.unity3d.com/Manual/GraphicsTutorials.html
    www.malbred.com/3d-grafika-3d-redaktory/sovremennaya-terminologiya-3d-grafiki/vertex-shader-vershinnyy-sheyder.html
    3dpapa.ru/accurate-displacement-workflow
    • +16
    • 4,4k
    • 2
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 2

      0
      Спасибо за статью!

      я постараюсь описать свою логику мышления при создании новых или сложных эффектов
      — вот это было бы очень интересно.
        +1

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

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое