Version: 2017.1
Writing Surface Shaders
Modelos de Iluminación Personalizados en Surface Shaders

Ejemplos del Surface Shader

He aquí algunos ejemplos de Surface Shaders. Los ejemplos de abajo se centran en utilizar modelos de iluminación integrados; ejemplos en cómo implementar modelos personalizados de iluminación se encuentran en Surface Shader Lighting Examples.

Simple

Nosotros vamos a comenzar con un shader muy simple y construir a partir de este. He aquí un shader que establece el color de la superficie a “blanco”. Utiliza un modelo de iluminación 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"
    }

He aquí cómo se ve en un modelo con dos lights establecidas:

Texture

Un objeto completamente blanco es bastante aburrido, entonces agreguemos un Properties block al shader, para que obtengamos un selector de textura en nuestro Material. Otros cambios están en negrillas a continuación.

  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"
    }

Normal mapping

Agreguemos algún normal mapping:

  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 (Iluminación Rim)

Ahora, intente agregar algún Rim Lighting para destacar los bordes del objeto. Nosotros vamos a agregar alguna luz emisiva basado en un ángulo entre la normal de la superficie y la dirección de la vista. Para esto, nosotros utilizaremos la variable surface shader integrada 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"
    }

Detail Texture (Textura en Detalle)

Para un efecto diferente, agreguemos un detail texture que es combinado con la textura base. Detail texture utiliza los mismos UVs, pero usualmente tienen diferentes tejas(Tiling) en el Material, entonces nosotros tenemos que utilizar diferentes coordenadas input 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"
    }

Utilizar una textura de ajedrez no tiene sentido práctico, pero ilustra lo que sucede:

Detail Texture en Screen Space

Qué tal una detail texture en screen space? No tiene mucho sentido para el modelo de cabeza de un soldado, pero ilustra cómo el input integrado screenPos puede ser utilizado:


  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"
    }

El normal mapping ha sido eliminado del sombreador de arriba, solo para hacerlo más corto:

Cubemap Reflection (Reflexión Cubemap)

He aquí un sombreador que hace una reflexión cubemap utilizando el input integrado worldRefl. En realidad es muy similar al sombreador integrado 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"
    }

Y ya que asigna el color de reflexión como emisivo(Emission), nosotros obtenemos un soldado bastante brillante:

Si usted quiere hacer reflexiones que no sean afectadas por normal maps, tiene que ser un poco más complicado:: INTERNAL_DATA necesita ser agregado a la estructura Input, y la función WorldReflectionVector utilizada para computar vectores de reflexión por-píxel después de que usted haya escrito el output de la 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"
    }

He aquí un normal mapped soldado brillante:

Slices (cortes) vía Posición del Espacio del Mundo

He aquí un sombreador que “corta” el objeto descartando píxeles en los aros horizontales cercanos. Esto se hace utilizando la función clip() Cg/HLSL basado en la posición en el mundo (world position) de un píxel. Nosotros utilizaremos la variable surface shader integrada 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"
    }

Normal Extrusion con Vertex Modifier

Es posible utilizar una función “vertex modifier” que modificará información entrante de los vértices en el vertex shader. Esto puede ser utilizado para animación procedimental, extrusión a lo largo de las normales y así. La directiva de compilación del surface shader vertex:functionName es utilizado para esto, con una función que toma el parámetro inout appdata_full.

He aquí un sombreador que mueve vértices a lo largo de sus normales por la cantidad especificada en el material:

  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"
    }

Mover los vértices a lo largo de sus normales hacen un soldado gordo:

Información personalizada computada por-vértice

Utilizar una función vertex modifier también es posible computar información personalizada en un vertex shader, que luego va a ser pasado a la función por-vértice del surface shader. La misma directiva de compilación vertex:functionName es utilizada, pero la función debería tomar dos parámetros: inout appdata_full y out Input. Usted puede llenar cualquier miembro input que no sea un valor integrado ahí.

Tenga en cuenta: Los miembros personalizados de input utilizados de estas manera no deben tener nombres comenzando con ‘uv’ o ellos no van a funcionar adecuadamente.

El ejemplo de abajo define un miembro personalizado float3 customColor, que es computado en una función vértice:

  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"
    }

En este ejemplo el customColor es establecido al valor absoluto de la normal:

Más usos prácticos puede ser computar cualquier información por-vértice que no esté dada por las variables input integradas; o optimizar computaciones de sombreador. Por ejemplo, es posible computar Rim Lighting en los vértices del objeto, en vez de hacer eso en el surface shader por-píxel.

Final Color Modifier (Modificador de Color Final)

Es posible utilizar una función “final color modifier” que modificará el color final(final color) computado por el sombreador. La directiva de compilación del Surface Shader finalcolor:functionName es utilizado para esto, con una función que toma parámetros Input IN, SurfaceOutput o, inout fixed4 color.

He aquí un sombreador simple que aplica tinte al color final (final color). Esto es diferente de simplemente aplicar tint al color Albedo de la superficie: este tinte va a también afectar cualquier color que haya venido de lightmaps, focos de luz(light probes) y fuentes extras similares.

  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"
    }

Custom Fog con Final Color Modifier

Un uso común para final color modifier (ver arriba) sería implementar una niebla(fog) completamente personalizada. La niebla necesita afectar el color final computado por el pixel shader, que es exactamente lo que el finalcolor modifier hace.

He aquí un sombreador que aplica tinte de niebla(fog) basado en distancias desde el centro de la pantalla. Esto combina ambos el vertex modifier con custom vertex data (fog) y final color modifier. Cuando es utilizado en un pase aditivo forward rendering, la niebla(Fog) necesita desvanecer a un color negro, y este ejemplo maneja esto como también una revisión para 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 = UnityObjectToClipPos(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"
    }

Linear Fog

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(UnityObjectToViewPos(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"
}

Decals

Los Decals son una manera común de agregar detalles a materiales en tiempo de ejecución (los impactos de balas son un ejemplo). Estos son una herramienta buena especialmente en deferred rendering ya que altera el GBuffer antes de que esté prendido, por lo tanto ahorra rendimiento.

En una escenario típico el Decal probablemente debería ser renderizado después de los objetos opacos y no debe ser un shadow caster (emisor de sombras) como se ve en las “Tags” shaderlab del ejemplo de abajo.

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
Modelos de Iluminación Personalizados en Surface Shaders