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

Примеры поверхностных шейдеров

Вот несколько примеров поверхностных шейдеров. Приведённые ниже примеры сосредоточены на использовании встроенных моделей освещения; примеры, показывающие, как реализовать пользовательские модели освещения, можно найти в примерах освещения поверхностных шейдеров.

Простой

Мы начнем с очень простого шейдера и на его основе создадим некоторые другие. Вот шейдер, который просто устанавливает “white” (белый) цвет поверхности. Он использует встроенную модель освещения Lambert (diffuse).

  Shader "Example/Diffuse Simple" {
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float4 color : COLOR;
      };
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = 1;
      }
      ENDCG
    }
    Fallback "Diffuse"
  }

Вот как это выглядит на модели с двумя источниками освещения:

Текстура

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

  Shader "Example/Diffuse Texture" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float2 uv_MainTex;
      };
      sampler2D _MainTex;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

Карты нормалей

Давайте добавим немного карт нормалей:

  Shader "Example/Diffuse Bump" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _BumpMap ("Bumpmap", 2D) = "bump" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
        float2 uv_MainTex;
        float2 uv_BumpMap;
      };
      sampler2D _MainTex;
      sampler2D _BumpMap;
      void surf (Input IN, inout SurfaceOutput o) {
        o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
        o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

Rim Lighting (подсветка краёв модели)

Теперь попробуем добавить немного Rim Lighting для подсветки краёв объекта. Мы добавим дополнительный излучающийся свет, зависящий от угла между нормалью поверхности и направлением взгляда. Для этого мы будем использовать встроенную переменную viewDir поверхностного шейдера.

  Shader "Example/Rim" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _BumpMap ("Bumpmap", 2D) = "bump" {}
      _RimColor ("Rim Color", Color) = (0.26,0.19,0.16,0.0)
      _RimPower ("Rim Power", Range(0.5,8.0)) = 3.0
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float2 uv_MainTex;
          float2 uv_BumpMap;
          float3 viewDir;
      };
      sampler2D _MainTex;
      sampler2D _BumpMap;
      float4 _RimColor;
      float _RimPower;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
          o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
          half rim = 1.0 - saturate(dot (normalize(IN.viewDir), o.Normal));
          o.Emission = _RimColor.rgb * pow (rim, _RimPower);
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

Текстура детализации

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

  Shader "Example/Detail" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _BumpMap ("Bumpmap", 2D) = "bump" {}
      _Detail ("Detail", 2D) = "gray" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float2 uv_MainTex;
          float2 uv_BumpMap;
          float2 uv_Detail;
      };
      sampler2D _MainTex;
      sampler2D _BumpMap;
      sampler2D _Detail;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
          o.Albedo *= tex2D (_Detail, IN.uv_Detail).rgb * 2;
          o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

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

Текстура детализации в экранном пространстве

Как насчет текстуры детализации в экранном пространстве? Это не имеет особого смысла для модели головы солдата, но зато показывает, как можно использовать встроенный ввод screenPos:

  Shader "Example/ScreenPos" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _Detail ("Detail", 2D) = "gray" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float2 uv_MainTex;
          float4 screenPos;
      };
      sampler2D _MainTex;
      sampler2D _Detail;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
          float2 screenUV = IN.screenPos.xy / IN.screenPos.w;
          screenUV *= float2(8,6);
          o.Albedo *= tex2D (_Detail, screenUV).rgb * 2;
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

Применение карт нормалей было удалено из приведённого выше шейдера, просто чтобы сделать его короче:

Отражение кубической текстуры

Вот шейдер, в котором реализовано отражение кубической текстуры используя встроенный ввод worldRefl. На самом деле, он очень похож на встроенный шейдер Reflective/Diffuse:

  Shader "Example/WorldRefl" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _Cube ("Cubemap", CUBE) = "" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float2 uv_MainTex;
          float3 worldRefl;
      };
      sampler2D _MainTex;
      samplerCUBE _Cube;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * 0.5;
          o.Emission = texCUBE (_Cube, IN.worldRefl).rgb;
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

И так как он присваивает цвет отражения Emission, у нас получается очень блестящий солдат:

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

  Shader "Example/WorldRefl Normalmap" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _BumpMap ("Bumpmap", 2D) = "bump" {}
      _Cube ("Cubemap", CUBE) = "" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float2 uv_MainTex;
          float2 uv_BumpMap;
          float3 worldRefl;
          INTERNAL_DATA
      };
      sampler2D _MainTex;
      sampler2D _BumpMap;
      samplerCUBE _Cube;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * 0.5;
          o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
          o.Emission = texCUBE (_Cube, WorldReflectionVector (IN, o.Normal)).rgb;
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

Вот блестящий солдат с применением карт нормалей:

Нарезка через положение в мировом пространстве

Вот шейдер, который “нарезает” объект путём отбрасывания пикселей в почти горизонтальных кольцах. Он делает это с помощью функции Cg/HLSL clip(), основанной на положении пикселя в мировом пространстве. Мы будем использовать встроенную переменную поверхностного шейдера worldPos.

  Shader "Example/Slices" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _BumpMap ("Bumpmap", 2D) = "bump" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      Cull Off
      CGPROGRAM
      #pragma surface surf Lambert
      struct Input {
          float2 uv_MainTex;
          float2 uv_BumpMap;
          float3 worldPos;
      };
      sampler2D _MainTex;
      sampler2D _BumpMap;
      void surf (Input IN, inout SurfaceOutput o) {
          clip (frac((IN.worldPos.y+IN.worldPos.z*0.1) * 5) - 0.5);
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
          o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

Выдавливание по нормалям с помощью модификатора вершины

Можно реализовать функцию “модификатор вершины”, которая будет изменять поступающие данные вершины в вершинном шейдере. Это может применяться для процедурной анимации, вытягивания вдоль нормалей и т.д. Для этого используется директива компиляции поверхностных шейдеров vertex:functionName с функцией, которая принимает параметр inout appdata_full.

Вот шейдер, который перемещает вершины вдоль их нормалей на указанную в материале величину.

  Shader "Example/Normal Extrusion" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _Amount ("Extrusion Amount", Range(-1,1)) = 0.5
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert vertex:vert
      struct Input {
          float2 uv_MainTex;
      };
      float _Amount;
      void vert (inout appdata_full v) {
          v.vertex.xyz += v.normal * _Amount;
      }
      sampler2D _MainTex;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

Перемещение вершин вдоль их нормалей делает солдата толстым:

Повершинное вычисление пользовательских данных

Используя функцию модификатора вершины, также можно вычислить пользовательские данные в вершинном шейдере, которые затем будут попиксельно переданы функции поверхностного шейдера. Используется та же самая директива компиляции vertex:functionName, но функция должна принимать два параметра: inout appdata_full и out Input. Вы можете менять любые созданные вручную (не встроенные) поля структуры Input.

Примечание: названия вручную созданных полей структуры Input не должны начинаться с ‘uv’, иначе они не будут правильно работать.

В приведённом ниже примере определяется вручную созданное поле структуры float3 customColor, которое вычисляется в вершинной функции:

  Shader "Example/Custom Vertex Data" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert vertex:vert
      struct Input {
          float2 uv_MainTex;
          float3 customColor;
      };
      void vert (inout appdata_full v, out Input o) {
          UNITY_INITIALIZE_OUTPUT(Input,o);
          o.customColor = abs(v.normal);
      }
      sampler2D _MainTex;
      void surf (Input IN, inout SurfaceOutput o) {
          o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
          o.Albedo *= IN.customColor;
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

В этом примере customColor установлен на абсолютную величину нормали:

Более практичным применением может быть расчёт любых повершинных данных, которые не предоставлены во встроенных переменных Input; либо оптимизация расчётов шейдера. Например, можно рассчитать Rim Lighting на вершинах объекта, вместо попиксельного расчёта в поверхностном шейдере.

Модификатор итогового цвета

Можно использовать функцию “изменение итогового цвета”, которая будет изменять итоговый цвет, рассчитанный шейдером. Для этого используется директива компиляции поверхностного шейдера finalcolor:functionName, с функцией, которая принимает параметры Input IN, SurfaceOutput o, inout fixed4 color.

Вот простой шейдер, который применяет оттенок (tint) к финальному цвету. Это отличается от применения оттенка к поверхностному Albedo цвету: этот оттенок также будет влиять на любой цвет, который “пришёл” из карт освещения, зондов освещения и других аналогичных экстра источников.

  Shader "Example/Tint Final Color" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _ColorTint ("Tint", Color) = (1.0, 0.6, 0.6, 1.0)
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert finalcolor:mycolor
      struct Input {
          float2 uv_MainTex;
      };
      fixed4 _ColorTint;
      void mycolor (Input IN, SurfaceOutput o, inout fixed4 color)
      {
          color *= _ColorTint;
      }
      sampler2D _MainTex;
      void surf (Input IN, inout SurfaceOutput o) {
           o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

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

Типичное применение модификатора итогового цвета (см. выше) - реализация полностью пользовательского тумана. Туман должен влиять на итоговый рассчитанный цвет пиксельного шейдера, что и делает в точности модификатор finalcolor.

Вот шейдер, который применяет оттенок тумана, основанный на расстоянии от центра экрана. Это совмещает и вершинный модификатор с пользовательскими данными вершин (fog) и модификатор итогового цвета. При использовании в проходе упреждающего рендеринга (forward rendering), туман должен уходить в чёрный цвет и в этом примере это учтено с помощью проверки на UNITY_PASS_FORWARDADD.

  Shader "Example/Fog via Final Color" {
    Properties {
      _MainTex ("Texture", 2D) = "white" {}
      _FogColor ("Fog Color", Color) = (0.3, 0.4, 0.7, 1.0)
    }
    SubShader {
      Tags { "RenderType" = "Opaque" }
      CGPROGRAM
      #pragma surface surf Lambert finalcolor:mycolor vertex:myvert
      struct Input {
          float2 uv_MainTex;
          half fog;
      };
      void myvert (inout appdata_full v, out Input data)
      {
          UNITY_INITIALIZE_OUTPUT(Input,data);
          float4 hpos = mul (UNITY_MATRIX_MVP, v.vertex);
          hpos.xy/=hpos.w;
          data.fog = min (1, dot (hpos.xy, hpos.xy)*0.5);
      }
      fixed4 _FogColor;
      void mycolor (Input IN, SurfaceOutput o, inout fixed4 color)
      {
          fixed3 fogColor = _FogColor.rgb;
          #ifdef UNITY_PASS_FORWARDADD
          fogColor = 0;
          #endif
          color.rgb = lerp (color.rgb, fogColor, IN.fog);
      }
      sampler2D _MainTex;
      void surf (Input IN, inout SurfaceOutput o) {
           o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;
      }
      ENDCG
    } 
    Fallback "Diffuse"
  }

Линейный туман

Shader "Example/Linear Fog" {
  Properties {
    _MainTex ("Base (RGB)", 2D) = "white" {}
  }
  SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 200
    
    CGPROGRAM
    #pragma surface surf Lambert finalcolor:mycolor vertex:myvert
    #pragma multi_compile_fog

    sampler2D _MainTex;
    uniform half4 unity_FogStart;
    uniform half4 unity_FogEnd;

    struct Input {
      float2 uv_MainTex;
      half fog;
    };

    void myvert (inout appdata_full v, out Input data) {
      UNITY_INITIALIZE_OUTPUT(Input,data);
      float pos = length(mul (UNITY_MATRIX_MV, v.vertex).xyz);
      float diff = unity_FogEnd.x - unity_FogStart.x;
      float invDiff = 1.0f / diff;
      data.fog = clamp ((unity_FogEnd.x - pos) * invDiff, 0.0, 1.0);
    }
    void mycolor (Input IN, SurfaceOutput o, inout fixed4 color) {
      #ifdef UNITY_PASS_FORWARDADD
        UNITY_APPLY_FOG_COLOR(IN.fog, color, float4(0,0,0,0));
      #else
        UNITY_APPLY_FOG_COLOR(IN.fog, color, unity_FogColor);
      #endif
    }

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

Деколь (Decal)

Decal are commonly used to add details to materials at runtime (bullets impacts are for example). They are a great tool especially in deferred rendering as they alter the GBuffer before it is lit, thus saving on performance.

In a typical scenario Decal should probably be rendered after the opaque objects and should not be a shadow caster as seen in the shaderlab “Tags” in the example below.

Shader "Example/Decal" {
  Properties {
    _MainTex ("Base (RGB)", 2D) = "white" {}
  }
  SubShader {
    Tags { "RenderType"="Opaque" "Queue"="Geometry+1" "ForceNoShadowCasting"="True" }
    LOD 200
    Offset -1, -1
    
    CGPROGRAM
    #pragma surface surf Lambert decal:blend
    
    sampler2D _MainTex;
    
    struct Input {
      float2 uv_MainTex;
    };
    
    void surf (Input IN, inout SurfaceOutput o) {
      half4 c = tex2D (_MainTex, IN.uv_MainTex);
      o.Albedo = c.rgb;
      o.Alpha = c.a;
    }
    ENDCG
  } 
Writing Surface Shaders
Пользовательские модели освещения в поверхностных шейдерах