Шейдер травы. Часть 2

Шейдер травы. Часть 2

Добро пожаловать во второй и, возможно, заключительный урок про шейдер травы (grass shader). Здесь мы расширим шейдер из первого урока и добавим несколько полезных функций, чтобы получить красивую траву!

В частности, мы добавим следующие функции:

  • Тесселяция для более пышной травы
  • Разнообразное освещение и полупрозрачность
  • Отбрасывание тени на траву и травой
  • Эффект небольшой примятости травы (но только лишь для одного объекта)

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

Обзор шейдера

grass shader от Гарри Алисавакиса

Алгоритм генерации травинок абсолютно такой же, как ранее, пока принимается во внимание геометрия и расположение. Но в геометрическом шейдере мы вычисляем две дополнительные вещи:

  • Генерация нормалей: так как мы подсветим травинки, нам нужно рассчитать вектор нормали для каждой вершины каждой травинки и передать его во фрагментный шейдер. Способ его вычисления довольно прост:
    • Нам нужен вектор, которые перпендикулярен травинке
    • Для травинки у нас есть 4 точки: две в основании (A и B), средняя точка в основании (M) и верхняя точка треугольника (C).
    • Вектор нормали N получается из векторного произведения AB и MC
нормали
  • Примятие травы: Взяв за основу положение и радиус, травинки смещены из первоначального положения как бы по сфере, чтобы имитировать приминание внешним объектом.

Тесселяция добавляется к шейдеру полностью так же, как у Roystan:

  • Вы берете «CustomTessellation.cginc» из великого тьюториала от Catlike Coding
  • Включаете ее в шейдер, используя #include “CustomTessellation.cginc”
  • Добавляете свойство float, под названием ”_TessellationUniform” к шейдеру
  • В проходах, помимо#pragma vertex vert и схожих директив, вы добавляете #pragma hull hull и #pragma domain domain.

Наконец, освещение и затенение:

  • Освещение: в качестве основной техники освещения я использовал полу-ламбертовский тип (NdotL * 0,5 + 0,5), чтобы иметь более мягкие тени. Имейте в виду, что, поскольку мы находимся в вершинном-(геометрическом-) фрагментном шейдере, не так просто заставить траву реагировать на все типы света или на любое их количество. Так что здесь я остановился только на первом направленном свете в сцене. Кроме того, чтобы сцена выглядела более однородной, ее окончательный цвет учитывает окружающее освещение.
  • Прозрачность: здесь очень-очень простой эффект полупрозрачности, который достигается путем получения скалярного произведения вектора направления зрения и инвертированного вектора направленного света. Результат затем умножается на цвет HDR для дополнительной прозрачности, и все это наносится на маску Френеля на траве.
  • Тени: трава сможет получать тени с помощью макроса «SHADOW_ATTENUATION» (подробнее об этом позже). Она также сможет отбрасывать тени через отдельный проход для отбрасывания теней.

Код

Давайте посмотрим, как все это выглядит в коде:

// Различные особенности этого шейдера взяты из Roystan's grass shader tutorial: https://roystan.net/articles/grass-shader.html
Shader "Geometry/GrassGeometryShader"
{
    Properties
    {
        // Цвет
        _Color("Color", Color) = (1,1,1,1)
        _GradientMap("Gradient map", 2D) = "white" {}
 
        // Тесселяция
        _TessellationUniform ("Tessellation Uniform", Range(1, 64)) = 1
         
        // Шум и ветер
        _NoiseTexture("Noise texture", 2D) = "white" {} 
        _WindTexture("Wind texture", 2D) = "white" {}
        _WindStrength("Wind strength", float) = 0
        _WindSpeed("Wind speed", float) = 0
        [HDR]_WindColor("Wind color", Color) = (1,1,1,1)
         
        // Положение и размеры
        _GrassHeight("Grass height", float) = 0
        _PositionRandomness("Position randomness", float) = 0
        _GrassWidth("Grass width", Range(0.0, 1.0)) = 1.0
 
        // Травинки
        _GrassBlades("Grass blades per triangle", float) = 1
        _MinimunGrassBlades("Minimum grass blades per triangle", float) = 1
        _MaxCameraDistance("Max camera distance", float) = 10
 
        // Освещение
        [Toggle(IS_LIT)]
        _IsLit("Is lit", float) = 0
        _RimPower("Rim power", float) = 1
        [HDR]_TranslucentColor("Translucent color", Color) = (1,1,1,1)
 
        // Приминание травы
        _GrassTrample("Grass trample (XYZ -> Position, W -> Radius)", Vector) = (0,0,0,0)
        _GrassTrampleOffsetAmount("Grass trample offset amount", Range(0, 1)) = 0.2
    }
    SubShader
    {
 
        CGINCLUDE
         
        #include "UnityCG.cginc"
        // Скачано с Catlike Coding: https://catlikecoding.com/unity/tutorials/advanced-rendering/tessellation/
        #include "CustomTessellation.cginc"
        #include "Autolight.cginc"
 
        struct appdata
        {
            float4 vertex : POSITION;
        };
 
        struct v2g
        {
            float4 vertex : POSITION;
        };
 
        struct g2f
        {
            float2 uv : TEXCOORD0;
            float4 vertex : SV_POSITION;
            float4 col : COLOR;
            float3 normal : NORMAL;
            unityShadowCoord4 _ShadowCoord : TEXCOORD1;
            float3 viewDir : TEXCOORD2;
        };
 
        fixed4 _Color;
        sampler2D _GradientMap;
         
        sampler2D _NoiseTexture;
        float4 _NoiseTexture_ST;
        sampler2D _WindTexture;
        float4 _WindTexture_ST;
        float _WindStrength;
        float _WindSpeed;
        fixed4 _WindColor;
 
        float _GrassHeight;
        float _GrassWidth;
        float _PositionRandomness;
 
        float _GrassBlades;
        float _MaxCameraDistance;
        float _MinimunGrassBlades;
 
        float4 _GrassTrample;
        float _GrassTrampleOffsetAmount;
 
        g2f GetVertex(float4 pos, float2 uv, fixed4 col, float3 normal) {
            g2f o;
            o.vertex = UnityObjectToClipPos(pos);
            o.uv = uv;
            o.viewDir = WorldSpaceViewDir(pos);
            o.col = col;
            o._ShadowCoord = ComputeScreenPos(o.vertex);
            o.normal = UnityObjectToWorldNormal(normal);
            #if UNITY_PASS_SHADOWCASTER
            o.vertex = UnityApplyLinearShadowBias(o.vertex);
            #endif
            return o;
        }
 
        float random (float2 st) {
            return frac(sin(dot(st.xy,
                                float2(12.9898,78.233)))*
                43758.5453123);
        }
 
        v2g vert (appdata v)
        {
            v2g o;
            o.vertex = v.vertex;
            return o;
        }
 
        //3 + 3 * 15 = 48
        [maxvertexcount(48)]
        void geom(triangle v2g input[3], inout TriangleStream triStream)
        {
            g2f o;
 
            float3 normal = normalize(cross(input[1].vertex - input[0].vertex, input[2].vertex - input[0].vertex));
            int grassBlades = ceil(lerp(_GrassBlades, _MinimunGrassBlades, saturate(distance(_WorldSpaceCameraPos, mul(unity_ObjectToWorld, input[0].vertex)) / _MaxCameraDistance)));
 
            for (uint i = 0; i < grassBlades; i++) {
                float r1 = random(mul(unity_ObjectToWorld, input[0].vertex).xz * (i + 1));
                float r2 = random(mul(unity_ObjectToWorld, input[1].vertex).xz * (i + 1));
 
                // Случайные барицентрические координаты https://stackoverflow.com/a/19654424
                float4 midpoint = (1 - sqrt(r1)) * input[0].vertex + (sqrt(r1) * (1 - r2)) * input[1].vertex + (sqrt(r1) * r2) * input[2].vertex;
                 
                r1 = r1 * 2.0 - 1.0;
                r2 = r2 * 2.0 - 1.0;
 
                float4 pointA = midpoint + _GrassWidth * normalize(input[i % 3].vertex - midpoint);
                float4 pointB = midpoint - _GrassWidth * normalize(input[i % 3].vertex - midpoint);
 
                float4 worldPos = mul(unity_ObjectToWorld, pointA);
 
                float2 windTex = tex2Dlod(_WindTexture, float4(worldPos.xz * _WindTexture_ST.xy + _Time.y * _WindSpeed, 0.0, 0.0)).xy;
                float2 wind = (windTex * 2.0 - 1.0) * _WindStrength;
 
                float noise = tex2Dlod(_NoiseTexture, float4(worldPos.xz * _NoiseTexture_ST.xy, 0.0, 0.0)).x;
                float heightFactor = noise * _GrassHeight;                        
 
                triStream.Append(GetVertex(pointA, float2(0,0), fixed4(0,0,0,1), normal));
 
                float4 newVertexPoint = midpoint + float4(normal, 0.0) * heightFactor + float4(r1, 0.0, r2, 0.0) * _PositionRandomness + float4(wind.x, 0.0, wind.y, 0.0);
 
                float3 trampleDiff = mul(unity_ObjectToWorld, newVertexPoint).xyz - _GrassTrample.xyz;
                float4 trampleOffset = float4(float3(normalize(trampleDiff).x, 0, normalize(trampleDiff).z) * (1.0 - saturate(length(trampleDiff) / _GrassTrample.w)) * random(worldPos), 0.0) * noise;
 
                newVertexPoint += trampleOffset * _GrassTrampleOffsetAmount;
                float3 bladeNormal = normalize(cross(pointB.xyz - pointA.xyz, midpoint.xyz - newVertexPoint.xyz));
 
                triStream.Append(GetVertex(newVertexPoint, float2(0.5, 1), fixed4(1.0, length(windTex), 1.0, 1.0), bladeNormal));
 
                triStream.Append(GetVertex(pointB, float2(1,0), fixed4(0,0,0,1), normal));
 
                triStream.RestartStrip();
            }
 
            for (int i = 0; i < 3; i++) {
                triStream.Append(GetVertex(input[i].vertex, float2(0,0), fixed4(0,0,0,1), normal));
            }
        }
 
        ENDCG
 
        Pass
        {
            Tags { "RenderType"="Opaque" "LightMode" = "ForwardBase" }
            Cull Off
            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment frag
            #pragma hull hull
            #pragma domain domain
            #pragma target 4.6
            #pragma multi_compile_fwdbase
            #pragma shader_feature IS_LIT
             
            #include "Lighting.cginc"
                         
            float _RimPower;
            fixed4 _TranslucentColor;
 
            fixed4 frag (g2f i) : SV_Target
            {
                fixed4 gradientMapCol = tex2D(_GradientMap, float2(i.col.x, 0.0));
                fixed4 col =  (gradientMapCol + _WindColor * i.col.g) * _Color;
                #ifdef IS_LIT
                float light = saturate(dot(normalize(_WorldSpaceLightPos0), i.normal)) * 0.5 + 0.5;
                fixed4 translucency = _TranslucentColor * saturate(dot(normalize(-_WorldSpaceLightPos0), normalize(i.viewDir)));
                half rim =  pow(1.0 - saturate(dot(normalize(i.viewDir), i.normal)), _RimPower);
                float shadow = SHADOW_ATTENUATION(i);
                col *= (light + translucency * rim * i.col.x ) * _LightColor0 * shadow + float4( ShadeSH9(float4(i.normal, 1)), 1.0) ;
                #endif 
                return col;
            }
             
            ENDCG
        }
 
        Pass
        {
            Tags {
                "LightMode" = "ShadowCaster"
            }
            CGPROGRAM
            #pragma vertex vert
            #pragma geometry geom
            #pragma fragment fragShadow
            #pragma hull hull
            #pragma domain domain
 
            #pragma target 4.6
            #pragma multi_compile_shadowcaster
 
            float4 fragShadow(g2f i) : SV_Target
            {
                SHADOW_CASTER_FRAGMENT(i)
            }            
             
            ENDCG
        }
 
    }
}

Свойства

Большинство свойств в шейдере имеют в точности то же назначение, что и в шейдере первой части, поэтому я просто расскажу о новых:

НазваниеНазначение
_TessellationUniform Определяет степень тесселяции меша
_IsLit Переключает ключевое слово «IS_LIT», которое определяет, будут ли применяться освещение и получение тени на траве
_RimPower Регулирует размер маски Френеля / светящегося ободка на травинках
_TranslucentColor Цвет HDR, который добавляется к травинкам для полупрозрачности
_GrassTrample 4D вектор, описывающий положение (компоненты XYZ) и радиус (компонента W) приминающего траву объекта
_GrassTrampleOffsetAmount Количество травинок, которые будут смещены внутри радиуса приминающего объекта

Промежуточные рассуждения

В начале, в строках 46 и 48 я должен включить 2 файла cginc: один для тесселяции (скачанный из этого тьюториала Catlike Coding) и «Autolight.cginc», который необходим для информации о получении теней. Это встроенный файл .cginc, так что вам не нужно добавлять его вручную, как первый.

Как и в первом уроке, структура appdata не обязательно должна содержать много информации для нас, только положение вершины. Структура «v2g» также остается неизменной, передавая только положение вершины геометрическому шейдеру.

Структура “g2f”, однако, имеет несколько дополнительных полей:

  • Поле “normal”, которое содержит вектор нормали, который мы вычисляем для каждой вершины
  • Поле “_ShadowCoord”, которое нужно для вычисления места попадания тени
  • “viewDir”, которое содержит вектор направления зрения

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

Как я упоминал в предыдущем уроке, этот шейдер имеет другую структуру: сначала есть блок CGINCLUDE, который содержит вершинный и геометрический шейдеры и все необходимые структуры, а затем есть 2 прохода: один для цвета и другой для отбрасывания тени. Хотя в первом уроке не было особой причины иметь такую структуру, здесь она весьма полезна, потому что нам нужны вершинный и геометрический методы на обоих проходах, и если бы мы не следовали этой структуре, нам пришлось бы переписать их в обоих проходах (или использовать внешний файл .cginc).

Метод «GetVertex» в строках 92-104 также получил еще кое-что: во-первых, в строке 96 я вычисляю направление зрения и сохраняю его в локальном объекте g2f, а в строке 98 я также сохраняю положение вершины на экране. Затем в строке 99 я сохраняю нормаль вершины после преобразования ее из пространства объектов в мировое пространство. Наконец есть странный блок в строках 100-102. Он говорит о том, что только во время прохода отбрасывания тени будет вызываться макрос «UnityApplyLinearShadowBias» для положения отсечения вершины. Это очень помогает с артефактами смещения тени на травинках, которые выглядят как раздражающие цветные полосы.

В строках 106-110 также есть случайный метод, как и раньше.

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

Геометрический шейдер

Я не буду вдаваться в подробности, потому что на 90% это то же самое, что и раньше. Я позаботился о том, чтобы каждая строка была такой же и находилась в том же положении, что и неосвещенный шейдер, поэтому изменения фактически начинаются со строки 153.

Однако следует отметить, что теперь в строке 149 я также передаю существующий вектор нормалей в метод «GetVertex» для первой точки нового треугольника.

Там я вычисляю направление, в котором должно быть смещена травинка, вычитая положение приминающего траву объекта (сохраненного в компонентах xyz _GrassTrample) из положения точки кончика травинки в мировом пространстве.

Смещение примятия затем рассчитывается (в строке 154) как приведенный результат строки 153 по осям x и z, умноженный на расстояние от центра, так что эффект не применяется, если травинки находятся дальше от приминающего объекта. Смещение также умножается на случайное число для некоторого разнообразия, и все это также умножается на шум.

В строке 156 смещение добавляется к положению точки кончика. Затем в строке 157 я вычисляю нормали для точки наконечника, используя метод, который я упомянул выше: получить приведенное векторное произведение вектора, образованного двумя базовыми точками, и вектора, образованного средней точкой и точкой кончика.

После этого в строке 159 я добавляю точку кончика, а в строке 161 я добавляю вторую вершину точки, и в строке 163 перезагружаю треугольный стрип.

Наконец, в строках 166-168 я добавляю исходные вершины грани к потоку, используя исходный вектор нормали.

Проход фрагментного шейдера

В строке 173 начинается фактический проход, который возвращает цвет травы и применяет освещение и затенение. Однако до фрагментного шейдера происходит еще несколько важных вещей.

Во-первых, в строке 175 важно добавить тег «LightMode» = «ForwardBase», чтобы происходило отбрасывание тени травой. В строке 176 я также отключаю отсечение, чтобы мы могли видеть все травинки.

В строках 178-185 есть несколько директив #pragma. Первые пять назначают вершинный, геометрический, фрагментный, поверхностный и зональный шейдеры соответствующим методам. Не беспокойтесь о местонахождении методов «поверхностный» и «зональный», они находятся в файле «CustomTessellation.cginc».

 «#Pragma target 4.6» помогает с поддержкой геометрического шейдера, в то время как «#pragma multi_compile_fwdbase» необходим для получения тени.

Наконец, есть директива #pragma shader_feature IS_LIT, которая добавляет вариант к шейдеру и использует ключевое слово «IS_LIT» для переключения между освещенной и неосвещенной конфигурацией.

Кроме того, в строке 187 я включаю «Lighting.cginc» (также встроенный, поэтому установка вручную не требуется), он необходим для некоторой информации об освещении, которую я использую. В строках 189-190 я заново объявляю свойства, которые не добавил в блок CGINCLUDE, так как буду использовать их только на этом этапе.

Фрагментный шейдер

Вот где применяется все окрашивание и шейдинги. Строки 194 и 195 такие же, как и в первой части, все еще применяя цвет на основе карты градиента, ветра и свойства _Color.

Переходя к освещенному блоку, в строке 197 я делаю простое вычисление по полу-Ламберту, получая скалярное произведение вектора направленного света и вектора нормали мирового пространства, которое затем умножаю на 0,5 и добавляю к нему 0,5. Это делается, чтобы не освещенная напрямую трава была не полностью темной, а имела более плавный градиент.

В строке 198 я вычисляю полупрозрачность, умножая свойство «_TranslucentColor» на скалярное произведение направления зрения и инвертированного вектора направленного света. Затем в строке 199 я вычисляю маску Френеля, используя довольно стандартную технику: делим скалярное произведение направления зрения и нормали, вычитаем результат из 1,0 и возводим в степень, чтобы контролировать ее насышенность / мощность.

В строке 200 я получаю затухание тени, а в строке 201 — результат освещения полу-Ламберта и добавляю к нему полупрозрачность, умноженную на светящийся ободок и красный канал цвета вершины (таким образом, она меняет интенсивность от верха к основанию травинки). Затем результат умножается на «_LightColor0» (который даст мне цвет направленного света с учетом интенсивности) и на затухание тени, рассчитанное выше. Затем я добавляю результат метода ShadeSH9, который даст мне информацию о цвете окружающего света и запеченных проб сцены. Исходный цвет затем умножается на результат всей этой операции, а затем возвращается в строке 203.

Проход отбрасывания тени

В строке 209 начинается проход, который заботится об отбрасывании тени. Во-первых, в строке 211 для этого есть специальный тег «LightMode» = «ShadowCaster», который помечает проход как соответствующий.

Позже, в строках 215-219 я добавляю те же директивы #pragma, что и раньше, с той лишь разницей, что вместо «#pragma fragment frag» я использую «#pragma fragment fragShadow», потому что буду использовать другую фрагментную функцию. Однако важно иметь другие прагмы, потому что проход для отбрасывания теней должен иметь ту же информацию о геометрии, что и первый проход, чтобы он мог правильно отбрасывать тени.

В строке 222 также важно включить «#pragma multi_compile_shadowcaster», чтобы отбрасывание теней действительно работало.

На этом этапе метод фрагментного шейдера очень прост: он лишь использует макрос «SHADOW_CASTER_FRAGMENT (i)», и это все, вам не нужно беспокоиться о том, что это делает.

Приминающий траву объект

Я забыл кое-что добавить. Скрипт, который должен иметь приминающий траву объект, чтобы трава реагировала. Это очень простой демонстрационный скрипт, вы могли бы сделать его разными способами, но вот мой:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
[ExecuteInEditMode]
public class GrassTramplePosition : MonoBehaviour {
    public Material material;
    public float radius;
    public float heightOffset;
 
    void Update() {
        material?.SetVector("_GrassTrample", new Vector4(transform.position.x, transform.position.y + heightOffset, transform.position.z, radius));
    }
}

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

Пакет Unity

Как и шейдер воды, grass shader можно сильно изменять, чтобы получить что-то красивое. Поэтому я также предоставлю вам пакет Unity с небольшой демонстрацией, чтобы было с чего начать:

Ссылка на Google drive с пакетом

Заключение

Этот grass shader может дать действительно классные результаты, однако мой подход не самый идеальный. Геометрические шейдеры в целом печально известны своей невысокой производительностью, и этот, в частности, не очень универсален, поскольку в нем нет какой-либо функции, которая позволяет вам иметь траву в определенных областях вместо всей сетки. Кроме того, геометрические шейдеры в действительности не работают на мобильных устройствах и Mac (насколько я знаю), поэтому они еще менее универсальны.

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

Я надеюсь, вы повеселились так же, как и я!

Код в тьюториалах по шейдерам распространяется по лицензии CC0, поэтому вы можете свободно использовать его в коммерческих и некоммерческих проектах без каких-либо упоминаний/благодарностей Гарри Алисавакису.

Источник: https://halisavakis.com/my-take-on-shaders-grass-shader-part-ii/

Понравилась статья? Поделиться с друзьями:
Автор natalya
Переводит для Вас самые интересные статьи про разработку игр. По образованию физик-программист. Техническими переводами начала подрабатывать еще на старших курсах и постепенно это переросло в основное занятие. Интересуется гуманитарными технологиями, пробует себя в журналистике.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *