원문: http://www.alanzucconi.com/2015/06/17/surface-shaders-in-unity3d/


(최대한 매끄러운 번역을 위해서 곳곳에 의역된 부분이나 추가/삭제된 내용이 존재하니 이 점 참고하시기 바랍니다.)



 Unity 셰이더에 대한 시리즈 그 두 번째입니다. 이번에는 서피스 셰이더(surface shader)에 대해서 자세히 들여다볼까 합니다. 이전 글에서 언급했듯이, 셰이더는 GPU에서 실행되는 프로그램으로서 흔히 Cg /HLSL 라는 언어로 작성됩니다. 이것을 통해, 화면 상 3D 모델의 삼각형들을 그리게 됩니다. 셰이더는 아주 간단히 말해서, 각각의 재질(머티리얼)들을 어떻게 렌더링할 지를 표현하는 코드입니다. 즉, 개발자들이 재질의 겉모습을 정의하기 위한 코딩 작업이죠. Unity에서는 개발자가 이 작업을 하기 편하도록 최대한 단순화시키기 위해 서피스 셰이더(surface shader)를 도입하였습니다.



 위의 다이어그램은 서피스 셰이더가 어떻게 동작하는지를 간략화하여 보여주고 있습니다. 먼저 3D모델이 3D모델 지오메트리를 변경할 수 있는 어떤 함수로 넘겨집니다. 그런 다음, 몇몇 직관적인 속성을 사용하여 겉모습을 정의하는 어떤 함수로 넘겨집니다 .(이 때에 필요한 다른 정보들도 함께 넘겨집니다.) 마지막으로, 이 속성들은 지오메트리가 근처의 광원들에 의해 어떻게 영향받을지를 결정하는 라이팅 모델에 의해 사용됩니다. 그 결과는 해당 모델의 각 픽셀들의 RGB 색상으로 나타납니다.




 서피스 함수(The surface function)

 서피스 셰이더의 핵심은 바로 서피스 함수(surface function)입니다. 서피스 함수는 3D모델의 데이터를 인풋으로 가져와서, 결과값으로 렌더링 프로퍼티(속성)들을 반환합니다. 아래의 서피스 셰이더는 오브젝트를 흰색 디퓨즈를 갖는 오브젝트로 렌더링합니다.


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; // 1 = (1,1,1,1) = white

      }

      ENDCG

    }

    Fallback "Diffuse"

  }


 코드에서 몇가지 살펴볼가요? 5번째 코드 라인을 보면 #pragma surface 지시자를 사용한 것을 볼 수 있습니다. 이를 해석하자면, "해당 셰이더를 위한 서피스 함수이름은 surf이고 라이팅 모델로는 램버시안 모델을 사용할 것이다"라는는 뜻을 담고 있습니다.  10번째 코드 라인은 머티리얼의 알베도(albedo) 즉, 재질 기본 색상이 흰색이라는 것을 나타냅니다. 이 서피스 함수는 최초 3D 모델로부터의 어떠한 데이터도 사용하지 않지만, 그럼에도 인풋 구조체를 정의하는 형식이 필요하다는 것을 유의하시기 바랍니다.




 서피스 아웃풋

 SurfaceOutput 구조체 안에는 머티리얼의 최종 모습을 결정하기 위해 사용되는 여러가지 프로퍼티들을 포함하고 있습니다:

  •  fixed3 Albedo: 기본 색상 / 오브젝트의 텍스처
  •  fixed3 Normal: 반사각을 결정하는 면의 방향
  •  fixed3 Emission: 이 오브젝트가 스스로 생성하는 빛의 양
  •  half Specular: 머티리얼이 빛을 반사하는 정도  (0~1) 
  •  fixed Gloss: 스펙큘러 반사가 퍼지는 정도
  •  fixed Alpha: 머티리얼의 투명한 정도

 Cg/HLSL은 전통적인 float 타입을 지원하긴 하지만, 여러분은 32비트 정밀도는 거의 필요하지 않을 것입니다. 아마도 16비트 정도면 충분할 것이기 때문에 half 타입을 주로 사용하게 될 것입니다. 또한 대부분의 파라미터가 0에서 1까지 범위 또는 -1에서 +1까지의 범위값을 가지는데, 이런 경우에는 fixed 타입은 사용하면 됩니다. 이 타입은 11비트를 사용하며 -2에서+2까지 커버합니다.


 더불어 Cg/HLSL은 이런 모든 타입들에 대하여 packed array를 지원합니다. 예를 들면, fixed2, fixed3, fixed4 이런 식으로 말입니다. 이런 타입들은 병렬 연산을 위하여 최적화됩니다. 그러므로 대부분의 일반적 연산들은 단 하나의 인스트럭션에 의해 gpu에서 수행될 수 있습니다. 아래의 네 가지의 결과는 모두 같습니다:

// Traditional C#

Albedo.r = 1;

Albedo.g = 1;

Albedo.b = 1;


// Packed arrays

Albedo.rgb = fixed3(1,1,1);

Albedo.rgb = 1;

Albedo = 1;




 텍스처 샘플링

 모델에 텍스처를 추가하는 것은 약간 더 복잡합니다. 코드를 보기 전에, 여러분은 3D 오브젝트 위에 텍스처 맵핑이 어떻게 일어나는지 이해해야 합니다. 텍스처를 입힐 모델은 여러 개의 삼각형들로 만들어졌고, 각각의 삼각형은 3개의 버텍스로 구성됩니다. 데이터는 이러한 각 버텍스들 안에 저장됩니다. 이 데이터에는 일반적으로 UV와 색상 데이터가 여기에 포함됩니다. UV는 텍스처의 어느 점이 해당 버텍스에 맵핑되는지를 나타내는 2D 벡터값입니다.  



 위의 왼쪽 그림은 Unity3D Bootcamp 데모에 나오는 군인의 모델입니다. 셰이드 와이어프레임(shaded wireframe)으로 렌더링된 모습입니다. 왼쪽 모델을 보면 두 개의 표시 삼각형을 볼 수 있습니다. 이 삼각형의 버텍스들을 통해서 바로 오른쪽 텍스처 상에 맵핑되는 0에서 1사이의 정규값을 가져올 수 있는데, 이것이 바로 텍스처 UV좌표입니다.  


 즉, 3D 오브젝트에 텍스처를 입히길 원한다면, 각 버텍스의 UV좌표가 필요합니다. 다음의 셰이더는 UV에 따라 텍스처를  모델에 맵핑합니다.


  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"

  }


 코드를 한번 살펴보겠습니다. 12번째 줄을 보면 _MainTex 라는 이름으로 텍스처 프로퍼티를 선언했습니다. 이 프로퍼티는 3번째 라인을 보면, 머티리얼 인스펙터를 통해 접근가능하도록 처리가 된 것을 알 수 있습니다. 현재 픽셀의 UV 데이터는 10번줄에서 추출됩니다. 이것은 Input 구조체 필드의 텍스처 이름에 uv라고 붙은 네이밍을 통해서 일어납니다 (여기서는 uv_MainTex).


 다음 단계는 uv가 참조할 텍스처 부분을 찾는 것입니다. Cg/HLSL은 tex2d라는 이것을 해주는 유용한 함수를 제공합니다. 텍스처와 uv값을 넘기면, 이에 해당하는 RGB 색상을 반환합니다. tex2d 함수는 텍스처를 임포팅할 때, Unity에 의해 직접 설정될 수 있는 다른 파라미터들을 계산에 고려합니다.




 서피스 인풋

 Cg/HLSL은 약간 흥미로운 기능을 가지고 있습니다. 서피스 인풋인 Input의 필드들은 Unity가 계산할 값들로 채울 수 있다는 점입니다. 예를 들어, float3 worldPos를 추가함으로써 surf 에서의 해당 점에 대한 월드 포지션을 가지고 초기화될 것입니다. 이것은 특정 점으로부터의 거리에 의존적인 이펙트를 생성하는 데에 사용됩니다.


Shader "Example/Diffuse Distance" {

Properties {

_MainTex ("Texture", 2D) = "white" {}

_Center ("Center", Vector) = (0,0,0,0)

_Radius ("Radius", Float) = 0.5

}

SubShader {

Tags { "RenderType" = "Opaque" }

CGPROGRAM

#pragma surface surf Lambert

struct Input {

float2 uv_MainTex;

float3 worldPos;

};

sampler2D _MainTex;

float3 _Center;

float _Radius;


void surf (Input IN, inout SurfaceOutput o) {

float d = distance(_Center, IN.worldPos);

float dN = 1 - saturate(d / _Radius);

if (dN > 0.25 && dN < 0.3)

o.Albedo = half3(1,1,1);

else

o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb;

}


ENDCG

Fallback "Diffuse"

}


 20, 21번째 줄을 보면, 우리가 머티리얼 인스펙터에서 정의했던 점(_Center)으로부터 그려질 픽셀과의 거리(IN.worldPos)를 계산합니다. 그런 다음, 그것을 0과 1 사이의 값으로 클램프하는 과정을 거칩니다. 결과값은 _Center에 가까울수록 1이고, _Radius에 가까울수록 0이 됩니다. 22번줄부터의 코드에서는, 거리가 일정 범위 안에 있을 때는 픽셀의 알베도를 흰색으로 정해버립니다.


 셰이더는 GPU에서 실행될 때, 순차적인 코드에 대해서 극도로 최적화된다는 점을 기억해야 합니다. 셰이더에 브랜치가 발생하는 코드를 추가하는 것은 퍼포먼스를 엄청나게 떨어뜨립니다. 두 브랜치에 대한 것을 모두 계산하고 그 결과를 섞는 것이 흔히 더 효율적입니다. 따라서 아래와 같은 형태의 코드로 변경할 수 있습니다:

 float d = distance(_Center, IN.worldPos);

 float dN = 1 - saturate(d / _Radius); 

 dN = step(0.25, dN) * step(dN, 0.3);

 o.Albedo = tex2D (_MainTex, IN.uv_MainTex).rgb * (1-dN) + half3(1,1,1) * dN;


  위의 코드와 같이, Cg/HLSL은 saturate와 step과 같은 많은 내장 함수를 가지고 있습니다. 이들을 이용함으로써, 대다수의 if문을 대체할 수 있다는 점을 기억하시기 바랍니다.




 그 밖의 다른 인풋들

 Cg는 worldPos와 같이, 특별한 용도로 사용할 수 있는 여러 가지 필드를 제공해 줍니다. Unity 공식 문서에 따르면, 다음에 나열된 필드들이 그 중 가장 많이 사용된다고 합니다:

  • float3 viewDir - 카메라의 방향(뷰 방향)입니다.
  • float4 name : COLOR - 이 구문을 사용함으로써, name 변수에 버텍스 색상을 포함할 것입니다.
  • float4 screenPos - 스크린 좌표 상의 해당 픽셀의 위치
  • float3 worldPos - 해당 픽셀에 대한 월드 좌표 상의 위치



 버텍스 함수

 서피스 셰이더이 가지고 있는 흥미로운 특징 중 하나는 버텍스들을 surf함수로 보내기 전에 이들을 수정/변경하는 능력입니다. surf함수가 RGBA 공간 안에서 색상을 다루는 반면, 공간에의 3D점을 제어하기 위해서는 버텍스 모디파이어(vertex modifier)라는 놈을 사용하는 방법을 알아야 합니다. 

 아주 쉬운 예를 살펴보겠습니다: 3D 모델을 원래의 모습보다 좀 더 통통하게 만드는 셰이더를 작성해 볼까요? 이렇게 만드려면, 모델의 삼각형을 삼각형-면이 향하는 방향을 따라서 확장시키면 됩니다. 삼각형-면이 향하는 방향은 결국 노멀(normal)값으로 나타나며, 삼각형 자신의 표면에 수직인 단위벡터입니다. 그렇다면, 이제 다음과 같이 노멀 방향으로 버텍스를 확장하려면 다음과 같은 수식을 사용할 수 있습니다: 

 newVertex = vertex + normal*amount

 위의 식에서 amount값은 버텍스가 갖는 위치 변동 정도를 나타냅니다. 이렇게 노멀 방향으로 버텍스를 확장하는 기법을 노멀 익스트루전(normal extrusion)이라고도 불리우니 참고하시기 바랍니다.


 Shader "Example/Normal Extrusion" {

    Properties {

      _MainTex ("Texture", 2D) = "white" {}

      _Amount ("Extrusion Amount", Range(-0.0001,0.0001)) = 0

    }

    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"

  }


 코드 9번째 줄을 보면, vert라는 이름을 갖는 버텍스 모디파이어(vertex modifier)를 설정하였다는 것을 알 수 있습니다. 실제 vert 함수 코드를 보면, 버텍스 위치를 받아서 노멀을 따라 해당 버텍스를 밀어냅니다. 코드 중에서, appdata_full 구조체는 해당 현재 버텍스의 모든 데이터를 포함하고 있습니다.




 Putting all together: the snow shader

 surf 함수와 vert함수 이 두 가지 모두를 사용하는 서피스 셰이더 한 예로는 스노우 이펙트(snow effect)가 있습니다. 이 효과는 어떤 모델의 삼각형 위에 눈(snow)이 쌓이는 것을 시뮬레이션하는 것입니다. 처음에는 _SnowDirection을 직접 마주하는 삼각형들에만 적용되다가, _Snow가 증가함에 따라서 하늘을 향하지 않는 삼각형들도 결국엔 영향을 받게 되는 방식으로 동작하도록 만들어 볼 수 있겠습니다.



우선, 여러분은 삼각형이 하늘을 향해 있다는 의미를 이해해야 합니다. _SnowDirection은 눈이 내리는 방향을 의미하며 단위벡터의 형태입니다. 그들이 어떻게 정렬되어 있는지를 확인할 수 있는 방법은 많습니다. 그러나 가장 쉬운 방법은 눈 방향으로 노멀을 투사하는 것입니다. 두 벡터 모두 크기는 1이므로, 결과값은 +1(같은 방향)에 -1(반대방향) 사이가 될 것입니다. 이런 방법에 대한 자세한 내용은 어차피 다음 튜토리얼에서 자세히 소개할 것입니다. 다만 지금은 이 연산이 dot 연산이라는 것이고, cosθ와 동일하다는 것만 알면 됩니다. 특정 _Snow값보다 도트 연산이 커지게 된다는 것은 ...


두 방향이 동일하면 cos θ >= +1 은 true입니다.

θ가 90도보다 작으면 cos θ >= 0 은 true입니다. 

cos θ >= -1 는 항상 true입니다.


 여기서 또 다른 정보가 필요합니다. _SnowDirection은 '월드 좌표'에서 방향을 표현하는 반면, 노멀은 일반적으로 '오브젝트 로컬 좌표' 안에서 표현됩니다. 따라서 이 둘은 서로 다른 좌표 시스템으로 매핑되어 있기 때문에, 값을 직접 비교할 수 없습니다. Unity는 WorldNormalVector라고 하는 함수를 제공하는데, 이 함수는 노멀을 월드 좌표로 맵핑해줍니다.  


Shader "Example/SnowShader" {

Properties {

    _MainColor ("Main Color", Color) = (1.0,1.0,1.0,1.0)

    _MainTex ("Base (RGB)", 2D) = "white" {}

    _Bump ("Bump", 2D) = "bump" {}

    _Snow ("Level of snow", Range(1, -1)) = 1

    _SnowColor ("Color of snow", Color) = (1.0,1.0,1.0,1.0)

    _SnowDirection ("Direction of snow", Vector) = (0,1,0)

    _SnowDepth ("Depth of snow", Range(0,0.0001)) = 0

}

SubShader {

    Tags { "RenderType"="Opaque" }

    LOD 200

 

    CGPROGRAM

    #pragma surface surf Lambert vertex:vert

 

    sampler2D _MainTex;

    sampler2D _Bump;

    float _Snow;

    float4 _SnowColor;

    float4 _MainColor;

    float4 _SnowDirection;

    float _SnowDepth;

 

    struct Input {

        float2 uv_MainTex;

        float2 uv_Bump;

        float3 worldNormal;

        INTERNAL_DATA

    };

 

    void vert (inout appdata_full v)

    {

    // Convert the normal to world coordinates

    float4 sn = mul(UNITY_MATRIX_IT_MV, _SnowDirection);

    if(dot(v.normal, sn.xyz) >= _Snow)

        v.vertex.xyz += (sn.xyz + v.normal) * _SnowDepth * _Snow;  

    }

 

    void surf (Input IN, inout SurfaceOutput o)

    {

        half4 c = tex2D (_MainTex, IN.uv_MainTex);

        o.Normal = UnpackNormal (tex2D (_Bump, IN.uv_Bump));

        if(dot(WorldNormalVector(IN, o.Normal), _SnowDirection.xyz)>=_Snow)

            o.Albedo = _SnowColor.rgb;

        else

            o.Albedo = c.rgb * _MainColor;

        o.Alpha = 1;

    }

    ENDCG

}

FallBack "Diffuse"

}


 벡터 사이의 코사인 값을 수동으로 계산하는 대신, Cg는 dot라는 이름의 도트 연산 구현을 포함하고 있습니다.


 코드 36번 줄은 노멀을 월드좌표로 바꾸기 위해서 조금 다른 메소드를 활용하고 있습니다. WorldNormalVector 함수는 버텍스 모디파이어 안에서는 사실상 이용할 수 없기 때문입니다.


 여러분이 정말 게임 안에서 눈(snow)이 필요하다면, Winter Shader와 같은 좀더 고급스러운 셰이더를 구매해서 사용하는 것을 고려해보시기 바랍니다.




 Conclusion

 이번 글에서는 서피스 셰이더를 소개하였습니다. 또한 이것을 가지고 어떻게 이펙트 연출 등에 사용될 수 있는지를 간단한 예로 살펴보았습니다. 이 글 내용은 유니티 메뉴얼의 서피스 셰이더 예제를 많이 참조하였습니다. 그 밖에도 라이팅 모델 구현하는 자세한 방법 소개하는 메뉴얼 페이지도 함께 있는데, 이 토픽은 세 번째 튜토리얼에서 상세히 살펴볼 것입니다.




Posted by 흑 기사
TAG ,

댓글을 달아 주세요