안녕하세요, 흑기사입니다.
다들 유니티로 게임 만드시느라 밤낮없이 매우 바쁘실텐데요.. 
너무 바쁜 나머지, 아직 유니티 셰이더 쪽은 살펴보지 못하신 분들께 조금이나마 도움이 되었으면 하는 마음으로..
유니티 셰이더에 대한 '기초'적인 내용들에 대해서 한번 정리해보았습니다. 
대부분의 내용은 유니티 메뉴얼을 참조해서 작성했음을 미리 말씀드립니다. 

 그럼 시작하겠습니다.

 유니티 셰이더에서 가장 먼저 알아야 할 것은 ShaderLab 입니다.
ShaderLab는 한마디로 유니티의 셰이더 스크립트 언어입니다. 보통 셰이더 언어라고 하면 Cg, HLSL, GLSL 에 더욱 익숙하실 텐데요. 유니티에서는 우선 기본적으로 ShaderLab을 기반으로 작성해야 합니다. 그럼 왜 하필 유니티에서는 ShaderLab이라는 것을 만들어 사용하고 있는 걸까요? (배워야 할 언어가 늘어날 수록 개발자에게 짜증을 유발합니다..)
 그건 유니티가 멀티플랫폼 엔진이니만큼 다양한 플랫폼, 다양한 디바이스들에 대응해야 하기 때문입니다. 어떤 특정 환경이나 상황에서도 초연하게 대응할 수 있는 공통의 인터페이스가 요구되므로, 이러한 셰이더 스크립트 언어를 사용하는 것입니다.  
 ShaderLab 문법은 처음에 확실하게 알고 가는 게 좋습니다. 유니티에서 셰이더를 작성하려면 어쨋든 ShaderLab의 영역은 벗어날 수가 없기 때문입니다. 

그럼 유니티 셰이더 파일의 기본 구조와 함께, 대표적인 ShaderLab 구문에 대해서 살펴보겠습니다.

Shader "MyCustomShader" { 
     SubShader { 
          // ...  body  ... // 
     } 
 위의 코드는 유니티 셰이더 파일의 가장 간단한 구조입니다. 하나의 Shader 블럭이 있고, 그 안에 SubShader가 있는 형태로 구성됩니다. "MyCustomShader"는 해당 셰이더의 이름입니다. 그리고 SubShader내의 body 라고 표기된 부분이 있는데, 여기에 실제 셰이더 코드(구현부)가 위치하게 됩니다. 즉, 우리는 어떤 셰이더를 작성하고 싶다면, 그 방법이 어떤 형태든지 간에 저 body 부분에 실제 코드를 작성하게 되는 겁니다. 
 여기서 한가지 의문이 들 수 있습니다. 그럼 그냥 간단하게 Shader{} 안에 body에 해당하는 셰이더 코드를 작성하면 될 것을, 왜 하필 귀찮게 또 SubShader{}라는 것을 만들고 거기에 코드를 작성해야 할까요?
 사실 Shader 블럭은 위의 예제처럼 하나의 SubShader 만이 아니라 여러 개의 SubShader로 구성될 수 있습니다. 아래와 같은 형태로 말입니다:

Shader "MyCustomShader" { 
     SubShader { 
          // ...  body  ... // 
     } 
     SubShader { 
          // ...  body  ... // 
     }       
     SubShader { 
          // ...  body  ... // 
     }

// ...other subshaders//
}
 이렇게 말이죠...
 결국 하나의 셰이더에서 실제 구현은 n개로 구성할 수 있다는 얘기입니다. 이렇게 여러 개의 구현을 할 수 있게 끔 구성한 이유는 그래픽 하드웨어(성능)의 다양성 때문입니다. 그래픽 하드웨어는 매우 다양하고, 그 성능도 제각각입니다. 그렇기 때문에 어떤 셰이더가 특정 하드웨어에서는 지원되지 않아 동작하지 않을 수 있습니다. 이런 상황을 막고 어떤 그래픽 하드웨어 환경이든 상관없이 동작하게 하기 위해서 여러 개로 다양한 구현을 할 수 있게 끔 인터페이스를 제공하는 것입니다. 어떤 셰이더에 대한 렌더링이 이루어 질 때, 유니티는 그 SubShader들의 리스트를 살펴본 뒤에 그 중에 현재 하드웨어가 지원 가능한 것들 중 가장 첫 번째 것을 사용하게 됩니다.

 코드에서 짚어보자면:
Shader "MyCustomShader" {  
      SubShader{  
          // A급 디바이스용 (최상의 품질로 구현) 
     }  
      SubShader{  
          // B급 디바이스용  
     }  
      SubShader{  
          // C급 디바이스용  
     }  
      SubShader{  
          // 그 밖의 초저사양 디바이스들 (매우 후진 품질로 구현)  
     }  

     Fallback "Diffuse"  
}
 대략 이런 식의 의도를 가지고 셰이더를 작성하게 되는 것입니다.

 

그런데 바로 위의 예제 코드를 다시 잘 살펴보면, 맨 아래쪽에 Fallback이라는 새로운 커맨드가 슬쩍 추가된 것을 볼 수 있습니다. 이 Fallback은 무슨 역할을 할까요? 
 만약 현재 셰이더의 모든 SubShader들이 현재 사용자의 그래픽 하드웨어에 맞지 않아 동작하지 않는 경우에 유니티 엔진은 Fallback을 통해서 지정된 이름의 셰이더를 찾아, 그 셰이더에서 현재 디바이스에서 동작 가능한 SubShader를 다시 한번 찾게 됩니다. 어찌보면 후보선수 같은 개념입니다. 원래 셰이더가 맞지 않을 때, 다른 대안 셰이더를 지정해 놓은 것이죠. 이런 식으로 각 셰이더들이 fallback셰이더들을 지정해 놓는다면, 최소한 아예 렌더링이 되지 않거나 하는 경우는 방지하게 되며 따라서 낮은 품질이라도 렌더링 자체는 보장할 수 있게 됩니다. 
 위의 예제에서는 "Diffuse"라는 이름의 셰이더를 Fallback 셰이더로 지정했습니다. 그래서 유니티가 "MyCustomShader"에서 적당한 SubShader를 찾지 못한 경우, "Diffuse"셰이더에서 SubShader를 찾는 작업을 반복하게 되는 것입니다.

 마지막으로 Pass구문에 대해서 알아보겠습니다. 이 Pass는 흔히 우리가 셰이더에서 익히 알고 있는 그 Pass와 동일합니다. 즉, 오브젝트의 렌더링의 단위를 의미하며, SubShader 안의 Pass의 수만큼 오브젝트가 렌더링됩니다. 그러므로 유니티에서는 Pass 구문을 통해서 경우에 따라서는 셰이더를 멀티 패스로 구현할 수 있습니다.

다시 정리해보면 일반적인 셰이더 구성은:
Shader "MyCustomShader" { 
     SubShader{ 
          Pass{ //  ...  // } 
          Pass{ //  ...  // } 
     } 

     SubShader{ 
          Pass{ //  ...  // } 
     } 

     //  ...other subshaders  // 

     Fallback "OtherShader" 
}
 대략 이 정도가 간단한 유니티 셰이더의 기본 형태가 되겠습니다. 
(사실 나중에 소개될 서피스 셰이더(surface shader)의 경우엔 Pass 구문은 필요하지 않지만, 우선 저렇게 알고 있는 게 편함.)


이제 마지막으로 실제로 화면에 뭔가를 보여주는 초간단한 셰이더 코드 하나 작성해보겠습니다.
Shader "Solid Red" { 
    SubShader { 
        Pass {  
            Color (1,0,0,1)  
        } 
    } 
 굉장히 심플합니다. "Solid Red" 라는 이름을 가진 셰이더이군요. 코드에는 특별한 것은 없고, Color라는 새로운 커맨드가 등장했습니다. Color 커맨드는 오브젝트에 단일 RGBA색상을 지정해주는 커맨드입니다. 따라서 위의 셰이더는 조건없이 그냥 오브젝트 전체를 그냥 빨간색으로 렌더링합니다. 하지만 뭔가 보여주는 셰이더치고는 너무 단순하고 쓸모없어 보이는군요.
 
다음 시간에는 ShaderLab을 이용하여, 약간 더 그럴듯한 셰이더를 제작하는 방법에 대해 알아보겠습니다.
감사합니다.


Posted by 흑 기사
TAG ,

댓글을 달아 주세요

  1. 동환 2016.10.21 14:17  댓글주소  수정/삭제  댓글쓰기

    와 진짜 설명 잘되어있네요. 항상 쓰면서 궁금했던 내용이 잘 설명되어 있어 많은 도움이됐습니다.
    정말 감사합니다. ㅠㅠ

  2. 소노 2016.10.31 04:37  댓글주소  수정/삭제  댓글쓰기

    명료한 설명과 간략한 튜토리얼 감사합니다