프로그래밍/Unity

유니티 셰이더 강좌 #3

windship 2014. 8. 15. 16:44

출처 : http://jinhomang.tistory.com/entry/%EC%9C%A0%EB%8B%88%ED%8B%B0-%EC%85%B0%EC%9D%B4%EB%8D%94%EC%9D%98-%EA%B8%B0%EC%B4%88-3


---


지난 시간에는 마지막에 fixed function shader 에 대해서 짧게 언급한 후 마무리 했었는데요..
 조금만 더 이야기 해보겠습니다.
 앞서 이미 얘기한 대로 fixed function shader는 이름에서도 느껴지듯이 고정기능 파이프 라인을 사용합니다. 이미 아실테지만, 원래 셰이더를 작성한다고 하면 당연히 프로그래머블 파이프라인을 떠올리는 것이 상식이죠. 즉, GPU의 버텍스 레벨과 픽섹 레벨 단계를 우리가 마음껏 코딩(버텍스셰이더, 픽셀셰이더)하여 원하는대로 멋대로 구현하는 것에 진정한 의미가 있습니다. 
 그런데 지금 현 시점에서 우리는 클래식한 고정기능 파이프 라인을 이용한 셰이더(fixed function shader)를 작성하고 있는 중입니다.
 이전 마지막 코드를 살펴볼까요,
Shader "VertexLit White" {  
    SubShader {  
        Pass {  
            Material {  
                Diffuse (1,1,1,1)  
                Ambient (1,1,1,1)  
            }  
            Lighting On  
        }  
    }  
}
 다시 들여다보니 정말 프로그래밍을 했다고 할 수 있는 부분은 보이지 않네요. 
 전부 그냥 무언가 제공되는 것에 대한 '설정'만을 했을 뿐입니다. 재질의 값을 설정하고 라이팅 기능을 그냥 켰을 뿐이고...음.. 고정기능 파이프라인을 사용한 것이 확실하군요... 역시 제대로 셰이더 코드를 작성했고 내세우기는 어려워 보이네요.
 게다가 고정기능 파이프라인에서는 버텍스당 라이팅(per-vertex lighting)을 사용합니다. 즉, 라이팅 연산이 버텍스 레벨에서 이루어진다는 것이죠. 따라서 픽셀 레벨에서의 라이팅 연산에 비해서는 연산 부하는 적겠지만, 당연히 그만큼 품질이 아주 떨어집니다. 더욱 슬픈 건 무엇인가를 개선하거나 수정하기 위한 프로그래머블한 방법은 없다는 것입니다. 고정기능 파이프라인이기 때문이지요!... 결국 할 수 있는 게 별로 없는 셰이더군요.    
그렇다면 fixed function shader를 사용하는 것은 진짜 쓸모없는 일일까요? (그럼 지금까지 뭘 한거죠.. ;)

 결론부터 말하자면 그렇지는 않습니다. 다행히 쓸모가 있습니다. 
 fixed function shader는 다양한 하드웨어에 대응에 꼭 필요합니다. 이미 예전에 언급한 적 있지만 우리는 다양한 플랫폼과 다양한 디바이스 환경에 대해 대응해야 합니다. 그러기 위해서 fixed function shader와 같은 저레벨 셰이더(?) 작성이 필요한 것이죠. 특히 오래된, 후진 그래픽 하드웨어 사양에 대한 fallback 셰이더로써 사용되는 데 아주 유용합니다. 유니티 빌트-인 셰이더들을 살펴보시면 아시겠지만, 최종적인 fallback은 fixed function shader를 두고 있습니다. 따라서 어떠한 디바이스에서도 대응할 수 있게 끔 구성해 놓은 것이죠. 
 그리고 품질을 대한 것은 논외로 하더라도 간단한 기능의 셰이더들은 fixed function shader로 제작하는 것이 더 수월할 수 있습니다. fixed function shader는 ShaderLab으로만 구성되기 때문에 쉽고 간결하게 깔끔히 작성할 수 있기 때문이죠. 그래서 알아두면 손해보지는 않을 거라고 생각이 됩니다.
 
지금까지 fixed function shader에 대한 이야기를 잠깐 해보았는데요...
앞으로 당분간은 ShaderLab만을 이용하여 제작할 수 있는 이 fixed function shader 에 대하여 계속 집중해보겠습니다.

 그럼 돌아가서 마지막에 작성한 셰이더를 다시 살펴봅니다.

Shader "VertexLit White" {  
    SubShader {  
        Pass {  
            Material {  
                Diffuse (1,1,1,1)  
                Ambient (1,1,1,1)  
            }  
            Lighting On  
        }  
    }  
}
 아, 우리가 이전에 구(sphere)에 적용했던 기억이 납니다. 조명에 영향받은 흰색 공을 화면에 띄웠었죠...
 근데 문득, 구(sphere)를 빨간색으로 해보면 더 멋질 것 같은 생각이 듭니다.
 그러려면 간단히 코드에서 재질의 Diffuse, Ambient 색상값을 변경하면 해결되겠군요:

Material {  
      Diffuse (1,0,0,1)  
      Ambient (1,0,0,1)  
}

아주 간단합니다.
그런데 이렇게 하면 재질 색상이 셰이더 항상 고정이 되버리네요. 여러 색상을 원할 경우, 그 색상 수만큼 셰이더를 만들고 각 오브젝트에 적용해야 한다는 얘기가 되네요. 이건 매우 비효율적일 수 밖에 없군요.
따라서 우리는 색상값을 고정시키지 말고 외부(유니티 에디터)에서 그 값을 읽어와서 셰이더에서 사용할 수 있도록 하는 것이 올바른 방법인 듯 합니다.  이것을 위해서 유니티는 Properties 구문을 제공합니다. Properties 안에 셰이더 외부로부터 개발자에 의해 설정될 수 있는 파라미터들을 정의하게 됩니다. 
 그러면 우선 코드에 Properties 구문을 추가해보겠습니다:

Shader "VertexLit Simple" { 
    Properties { 
        _MyColor ("Main Color", COLOR) = (0,0,1,1
    } 
    SubShader { 
        Pass { 
            Material { 
                Diffuse [_MyColor] 
                Ambient [_MyColor] 
            } 
            Lighting On 
        } 
    } 

위의 코드를 보면 새롭게 Properties 블럭이 추가된 것을 보실 수 있습니다. 1개의 파라미터 변수가 추가되었군요.
이 부분만 살펴보자면:

_MyColor ("Main Color", COLOR) = (0,0,1,1)

 _MyColor는 속성 변수의 이름을 나타내고, "Main Color"는 에디터 상에서 보여질 라벨(이름)입니다. COLOR는 변수가 색상타입임을 의미하며, 에디터 상에서 디폴트 값은 파란색(0,0,1,1)으로 지정함을 의미합니다.

 이렇게 Properties 블럭이 추가됨에 따라서 에디터에는 어떤 변화가 일어났는지 한번 보겠습니다.

재질 인스펙터 창에 뭔가 한 줄 추가된 것이 보이네요. "Main Color"라는 색상을 설정할 수 있는 인터페이스가 생겼고, 디폴트 색으로 현재 파란색이 설정되었습니다. 앞서 Properties 블럭에 추가한 내용이 반영되었다는 것을 알 수 있습니다. 예상한대로 에디터의 "Main Color" 값을 변경하면,  셰이더의 _MyColor 값에 그 값이 적용됩니다.

 그럼 이제 이 값을 사용하려면, 셰이더에서는 어떻게 작성해야 할까요?
 셰이더 내부에서는 [ 프로퍼티 이름 ] 이라고 작성하여서 그 값을 사용할 수 있습니다.
 그냥 위 코드의 일부분을 보면 알 수 있습니다:

 Material { 
           Diffuse [_MyColor] 
           Ambient [_MyColor] 
          }

 재질의 Diffuse와 Ambient 색상을 (r,g,b,a) 에서 [_MyColor]로 수정하였군요. 즉,  _MyColor 값을 참조하여  색상값을 지정하겠다는 것이죠.
 자, 드디어 에디터로부터 언제든 원하는 색상으로 오브젝트의 색상을 지정할 수 있게 되었습니다.
 물론 여기에서 사용한 COLOR 타입 프로퍼티 외에도 다양한 타입의 프로퍼티들이 존재하며, 셰이더에서는 그것들을 사용할 수 있습니다. 나머지 프로퍼티 타입들은 앞으로 계속 등장할테니 그때마다 익히면 되겠습니다.


 이제 조금 더 욕심이 생기는군요. 이젠 단순히 재질의 색상을 정하는 것 만이 아니라, 이제 표면에 텍스처 이미지를 입혀보려 합니다. 이것은 ShaderLab의 SetTexture를 이용하면 됩니다. 
 그전에 먼저 우리는 사용할 텍스처를 에디터에서 설정할 필요가 있습니다. 그러기 위해서 방금 전에 배웠던 프로퍼티를 활용하면 됩니다. Properties 블럭 안에 다음과 같이 프로퍼티 변수를 하나 추가하겠습니다.

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

 벌써 새로운 프로퍼티 타입이 등장했군요. 텍스처를 위한 2D 타입입니다. 디폴트 값으로 "white"가 지정되어 있는게 보입니다. 이것은 만약 개발자가 에디터를 통해 텍스처 이미지를 설정하지 않았을 때, 이 값을 어떻게 취급할 지를 결정합니다. "white"니까 흰 색상값으로 취급하겠군요. 이외에도 "black", "gray", "bump" 를 설정할 수 있으며, "" 도 가능합니다.
 { } 안에는 별도의 옵션이 들어갑니다. 이 옵션에 관해서는 이번에는 다루지 않고, 다음 번에 기회가 될 때 다루도록 하겠습니다.

에디터는 다음과 같이 변화가 생겼습니다:

Texture 프로퍼티가 추가된 것을 확인할 수 있군요. (텍스처는 임의로 맨홀 뚜껑 이미지를 세팅한 상태입니다.)

 그럼 이제 에디터에서 설정한 텍스처 이미지를 사용하기 위해서, 앞서 잠깐 언급된 SetTexture 라는 커맨드를 사용해보겠습니다. 한 가지 유의할 점은 우리가 텍스처를 적용하는 것은 색상을 결정하는 최종단계에서 이루어지므로, 코드의 마지막 부분에 들어가야 한다는 것입니다. 어쨋든:

SetTexture[_MainTex]

이렇게 추가하면, 우리가 에디터에서 설정한 _MainTex 를 가져와 적용하게 됩니다.

그럼 코드는:
Shader "VertexLit Simple" { 
    Properties { 
        _MyColor ("Main Color", COLOR) = (1,1,1,1
        _MainTex ("Base Texture"2D) = "white" {} 
    } 
    SubShader { 
        Pass { 
            Material { 
                Diffuse [_MyColor] 
                Ambient [_MyColor] 
            } 
            Lighting On 

            SetTexture [_MainTex]  
        } 
    } 

적용한 결과는:


그런데 뭔가 이상하게 보입니다. 라이팅 연산이 반영되지 않은 것도 같고...
아, 그러고보니 그냥 밋밋하게 최종 렌더링 색상으로 텍스처 이미지 색상 만이 적용되어 버렸네요.

Pass의 끝부분에 단순히 SetTexture [프로퍼티] 만 작성해놓았더니, 이전에 재질과 빛이 라이팅 연산을 통해 나온 결과 색상값은 그냥 무시해버리고, 현재 텍스처로 덮어씌워지는 현상이 발생했군요.

사실 우리는 텍스처를 설정하면서, 이전에 계산된 결과 색상값과 현재 텍스처가 어떻게 결합할 지를 결정해야 합니다. 그래야 적절히 믹스되어서 그럴듯한 색상으로 최종값이 결정되는 것이지요. 일단 다음과 같이 다시 코드를 추가해보겠습니다.

SetTexture [_MainTex] { Combine texture * primary } 

SetTexture 커맨드에 새로운 블럭을 추가되었습니다. 블럭 안에는 어떻게 색상을 결합할 지를 정하는 코드가 들어가며, Combine 커맨드로 설정하게 됩니다. texture는 현재 설정한 텍스처의 색상을 의미하고,  primary는 이전 라이팅연산되어 나온 색상을 의미합니다. 그러므로 이전 결과값과 현재 텍스처를 곱하겠다는 얘기로 해석되는군요. 
근데 여기서 조금 더 코드를 추가해볼까요.. 조명 강도를 좀 높이기 위해서 결과값을 두 배로 만들어주게끔 DOUBLE 을 추가합니다.DOUBLE은 결과값을 x2 해주는 키워드입니다.

SetTexture [_MainTex] { Combine texture * primary DOUBLE }


그럼 이제 최종 코드는:
Shader "VertexLit Simple" { 
    Properties { 
        _MyColor ("Main Color", COLOR) = (0,0,1,1
        _MainTex ("Base Texture"2D) = "white" {} 
    } 
    SubShader { 
        Pass { 
            Material { 
                Diffuse [_MyColor] 
                Ambient [_MyColor] 
            } 
            Lighting On 

            SetTexture [_MainTex] { 
                    Combine texture * primary DOUBLE 
            } 
        } 
    } 

실제 적용해본 결과입니다:


자, 이번에는 여기까지입니다.
다음 시간에는 더더욱 그럴 듯한 셰이더를 작성해보도록 하겠습니다.
감사합니다~