Version: 5.3 (switch to 5.4b)
Примеры освещения в поверхностных шейдерах
Программирование вершинных и фрагментных (пиксельных) шейдеров

Поверхностные шейдеры с использованием тесселяции DX11

В поверхностных шейдерах есть некоторая поддержка GPU тесселяции DirectX 11. Идея такова:

  • Тесселяция определяется модификатором tessellate:FunctionName. Эта функция рассчитывает факторы тесселяции на границе и внутри треугольника.
  • При использовании тесселяции, после неё вызывается “вершинный модификатор” (vertex:FunctionName) для с каждой сгенерированной вершины в доменном шейдере. Здесь вы обычно совершили бы наложение карт смещения.
  • Опционально поверхностные шейдеры могут производить расчёт phong тесселяции для сглаживания поверхности модели, даже без наложения карт смещения.

Текущие ограничения поддержки тесселяции:

  • Только треугольный домен - нет квадов, нет isoline тесселяции.
  • Когда используется тесселяция, шейдер автоматически компилируется с использованием Shader Model 5.0, потому он будет работать только под DX11.

Нет GPU тесселяции, смещение в модификаторе вершин

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

    Shader "Tessellation Sample" {
        Properties {
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp nolightmap
            #pragma target 5.0

            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            sampler2D _DispTex;
            float _Displacement;

            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }

            struct Input {
                float2 uv_MainTex;
            };

            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

Шейдер, приведённый выше, весьма стандартен, интересные места:

  • Вершинный модификатор disp совершает выборку из карты смещений и сдвигает вершины вдоль их нормалей.
  • Используется пользовательская структура “ввод вершинных данных” (appdata) вместо структуры по умолчанию appdata_full. Пока она не требуется, но для тесселяции эффективней использовать как можно меньшую структуру.
  • Так как наши вершинные координаты не имеют 2й UV координаты, мы добавляем директиву nolightmap для исключения карт освещения.

Вот как некоторые простые объекты будут выглядеть при использовании этого шейдера:

Фиксированное количество тесселяции

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

    Shader "Tessellation Sample" {
        Properties {
            _Tess ("Tessellation", Range(1,32)) = 4
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessFixed nolightmap
            #pragma target 5.0

            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            float _Tess;

            float4 tessFixed()
            {
                return _Tess;
            }

            sampler2D _DispTex;
            float _Displacement;

            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }

            struct Input {
                float2 uv_MainTex;
            };

            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

Функция тесселяции tessFixed в нашем шейдере возвращает четыре фактора тесселяции в виде одного значения float4: три фактора для каждой стороны треугольника и один фактор для внутренне области треугольника. Здесь мы просто возвращаем постоянное значение, которое указано в свойствах материала.

Тесселяция на основе расстояния

Мы также можем менять уровень тесселяции в зависимости от расстояния до камеры. Например, мы могли бы объявить два значения расстояния; расстояние, на котором тесселяция максимальная (скажем, 10 метров), и расстояние, по мере приближения к которому уровень тесселяции плавно падает (скажем, 20 метров).

    Shader "Tessellation Sample" {
        Properties {
            _Tess ("Tessellation", Range(1,32)) = 4
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessDistance nolightmap
            #pragma target 5.0
            #include "Tessellation.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            float _Tess;

            float4 tessDistance (appdata v0, appdata v1, appdata v2) {
                float minDist = 10.0;
                float maxDist = 25.0;
                return UnityDistanceBasedTess(v0.vertex, v1.vertex, v2.vertex, minDist, maxDist, _Tess);
            }

            sampler2D _DispTex;
            float _Displacement;

            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }

            struct Input {
                float2 uv_MainTex;
            };

            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

Здесь функция тесселяции получает три параметра; данные вершин трёх углов треугольника до тесселяции. Это необходимо для расчёта уровней тесселяции, которые зависят от текущего положения вершин. Мы включаем встроенный вспомогательный файл Tessellation.cginc и вызываем из него функцию UnityDistanceBasedTess для совершения всей работы. Эта функция рассчитывает расстояние до камеры каждой вершины и извлекает конечные факторы тесселяции.

Тесселяция на основе длины стороны

Тесселяция, основанная только на расстоянии хороша только когда размеры треугольников довольно похожи. На изображении, представленном выше, вы можете заметить, что у объектов с маленькими треугольниками тесселяция слишком сильная, в то время как объекты с большими треугольниками недостаточно тесселированы.

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

    Shader "Tessellation Sample" {
        Properties {
            _EdgeLength ("Edge length", Range(2,50)) = 15
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _DispTex ("Disp Texture", 2D) = "gray" {}
            _NormalMap ("Normalmap", 2D) = "bump" {}
            _Displacement ("Displacement", Range(0, 1.0)) = 0.3
            _Color ("Color", color) = (1,1,1,0)
            _SpecColor ("Spec color", color) = (0.5,0.5,0.5,0.5)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf BlinnPhong addshadow fullforwardshadows vertex:disp tessellate:tessEdge nolightmap
            #pragma target 5.0
            #include "Tessellation.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float4 tangent : TANGENT;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            float _EdgeLength;

            float4 tessEdge (appdata v0, appdata v1, appdata v2)
            {
                return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
            }

            sampler2D _DispTex;
            float _Displacement;

            void disp (inout appdata v)
            {
                float d = tex2Dlod(_DispTex, float4(v.texcoord.xy,0,0)).r * _Displacement;
                v.vertex.xyz += v.normal * d;
            }

            struct Input {
                float2 uv_MainTex;
            };

            sampler2D _MainTex;
            sampler2D _NormalMap;
            fixed4 _Color;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Specular = 0.2;
                o.Gloss = 1.0;
                o.Normal = UnpackNormal(tex2D(_NormalMap, IN.uv_MainTex));
            }
            ENDCG
        }
        FallBack "Diffuse"
    }

Здесь снова, мы просто вызываем функцию UnityEdgeLengthBasedTess из Tessellation.cginc для совершения всей работы.

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

Phong тесселяция

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

Поверхностные шейдеры Unity могут автоматически рассчитывать phong тесселяцию с помощью директивы компиляции tessphong:VariableName. Вот пример шейдера:

    Shader "Phong Tessellation" {
        Properties {
            _EdgeLength ("Edge length", Range(2,50)) = 5
            _Phong ("Phong Strengh", Range(0,1)) = 0.5
            _MainTex ("Base (RGB)", 2D) = "white" {}
            _Color ("Color", color) = (1,1,1,0)
        }
        SubShader {
            Tags { "RenderType"="Opaque" }
            LOD 300
            
            CGPROGRAM
            #pragma surface surf Lambert vertex:dispNone tessellate:tessEdge tessphong:_Phong nolightmap
            #include "Tessellation.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float2 texcoord : TEXCOORD0;
            };

            void dispNone (inout appdata v) { }

            float _Phong;
            float _EdgeLength;

            float4 tessEdge (appdata v0, appdata v1, appdata v2)
            {
                return UnityEdgeLengthBasedTess (v0.vertex, v1.vertex, v2.vertex, _EdgeLength);
            }

            struct Input {
                float2 uv_MainTex;
            };

            fixed4 _Color;
            sampler2D _MainTex;

            void surf (Input IN, inout SurfaceOutput o) {
                half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
                o.Albedo = c.rgb;
                o.Alpha = c.a;
            }

            ENDCG
        }
        FallBack "Diffuse"
    }

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

Примеры освещения в поверхностных шейдерах
Программирование вершинных и фрагментных (пиксельных) шейдеров