표면 셰이더 작성(Writing Surface Shaders)
표면 셰이더(Surface Shaders)에서의 커스텀 조명(Lighting) 모델

표면 셰이더(Surface Shader) 예제

다음은 Surface Shaders의 몇 가지 예제입니다. 아래 예제는 빌트인 조명 모델 사용에 주목합니다. 커스텀 조명 모델 구현 방법 예제는 표면 셰이더 조명 예제에서 확인해야 합니다.

간단한 셰이더(Shader)

우선 아주 간단한 셰이더에서 시작해서 점점 발전시켜 보겠습니다. 아래의 셰이더는 표면 컬러를 “흰색”으로 설정합니다. 이 셰이더는 빌트인 램버트(디퓨즈) 조명 모델을 사용합니다.

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

다음은 두 개의 광원이 설정된 모델에서 셰이더가 어떻게 보이는지를 나타냅니다.

텍스처(Texture)

전체가 흰색인 오브젝트는 밋밋해 보이므로 텍스처를 추가해 보겠습니다. 프로퍼티 블록을 셰이더에 추가하여 머티리얼에 텍스처 선택기를 둡니다. 기타 변경사항은 아래에서 굵은 글씨로 표시합니다.

  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)

이번에는 림 조명을 추가하여 게임 오브젝트의 가장자리를 강조하겠습니다. 표면 노멀과 뷰 방향 사이의 각도에 기반하여 발광 광원을 추가합니다. 이를 위해 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)

다른 효과를 추가하기 위해 베이스 텍스처와 결합된 Detail 텍스처를 추가합시다. Detail 텍스처는 동일한 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"
  }

텍스처 체커 사용이 늘 적합한 건 아니지만 이 예제에서는 무슨 일이 일어나는지 보기 위해서 사용하겠습니다.

스크린 공간(Screen Space)의 Detail 텍스처(Texture)

스크린 공간의 Detail 텍스처에 대해 알아보겠습니다. 이 텍스처는 병사의 머리 모델에는 별로 적합하지 않지만 여기선 빌트인 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"
  }

더 간단하게 만들기 위해 위 셰이더에서 노멀 매핑을 제거했습니다.

큐브맵 반사(Cubemap Reflection)

다음은 빌트인 worldRefl 입력을 사용하여 큐브맵 반사를 하는 셰이더입니다. 빌트인 반사/디퓨즈 셰이더와 매우 비슷합니다.

  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_DATAInput 구조에 추가되어야 하고 Normal 출력을 사용한 후에 WorldReflectionVector 함수가 픽셀당 반사 벡터를 계산해야 합니다.

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

다음은 노멀맵 처리된 빛나는 병사 모델입니다.

월드 공간 포지션을 통한 슬라이스

다음 셰이더는 수평 방향의 고리 형태로 픽셀을 제거하여 게임 오브젝트를 “슬라이스”합니다. 셰이더는 픽셀의 월드 포지션에 기반한 clip() Cg/HLSL 함수를 사용하여 슬라이스합니다. 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 Modifier)를 사용한 노멀 익스트루전(Extrusion)

버텍스 셰이더에 입력된 버텍스 데이터를 수정하는 “버텍스 모디파이어” 기능을 사용할 수 있습니다. 이는 순차적 애니메이션 노멀을 따라가는 익스트루전 등에 사용할 수 있습니다. 이를 위해 표면 셰이더 컴파일 지시자 vertex:functionNameinout 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_fullout 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는 노멀의 절대값으로 설정됩니다.

더 실용적인 사용법의 예로 빌트인 입력 변수에서 제공되지 않는 버텍스당 데이터를 연산이나 셰이더 연산 최적화 등이 있습니다. 예를 들어 표면 셰이더에서 픽셀당으로 계산하는 대신 게임 오브젝트의 버텍스에서 림 조명을 연산할 수 있습니다.

최종 컬러 모디파이어(Final Color Modifier)

셰이더가 계산한 최종 컬러를 수정하는 “최종 컬러 모디파이어” 기능을 사용할 수 있습니다. 이를 위해 표면 셰이더 컴파일 지시자 finalcolor:functionNameInput IN, SurfaceOutput o, inout fixed4 color를 파라미터로 받는 함수를 사용합니다.

다음은 최종 컬러에 틴트를 적용하는 간단한 셰이더입니다. 이는 표면 알베도 컬러에만 틴트를 적용하는 것과는 다릅니다. 이 틴트는 라이트맵, 라이트 프로브 및 유사한 별도 소스에서 나오는 모든 컬러에 영향을 줍니다.

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

최종 컬러 모디파이어(Final Color Modifier)를 사용한 커스텀 안개(Fog)

최종 컬러 모디파이어(위 참조)의 일반적인 용도는 포워드 렌더링에서 완전한 커스텀 안개를 구현합니다. 안개는 최종 계산된 픽셀 셰이더 컬러에 영향을 주어야 하며, 이 기능이 바로 finalcolor 모디파이어의 기능입니다.

아래 셰이더는 화면 중앙으로부터의 거리에 기반하여 안개 틴트를 적용합니다. 이는 커스텀 버텍스 데이터(fog)가 있는 버텍스 모디파이어와 최종 컬러 모디파이어를 결합합니다. 포워드 렌더링 추가 패스에서 사용되면 안개는 검은색으로 페이드되어야 하며 아래 예제에서는 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)

데칼은 흔히 런타임에서 머티리얼에 세부 사항을 추가하기 위해 사용합니다(예: 총알 임팩트). 특히 디퍼드 렌더링에서 훌륭한 툴로 사용되는데 조명 받기 전에 GBuffer를 변경하여 성능을 유지하기 때문입니다.

일반적인 시나리오에서 데칼은 불투명한 오브젝트 다음에 렌더링되며 아래 예제의 ShaderLab “Tags”에서 볼 수 있는 것처럼 그림자를 투영해서는 안 됩니다.

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)
표면 셰이더(Surface Shaders)에서의 커스텀 조명(Lighting) 모델