본문 바로가기
그래픽/TA STUDIO (동아리 활동)

[TA STUDIO] #1. '쉐이더'를 이용한 '이펙트' 기초 (포스트 프로세싱: bloom 효과)

by 케이이이이 2019. 10. 4.

본 포스팅은 청강문화산업대학교의 게임 제작 동아리 L.A.B.S

TA STUDIO에서 공부한 내용을 바탕으로 작성되었습니다.

 


 

#쉐이더? 그게 뭔데?

익숙하지만 생소한 단어. 쉐이더.

많이 들어봤지만, 정확히 뭐다- 라며 정의내리기는 쉽지 않죠.

"그거 그... 게임 때깔 이뻐지는... 그런 거 아냐?"
"마인크래프트 쉐이더?"

...이대로 넘어가면 문제가 있겠죠?

게임 개발자들의 신적 존재이자 영원한 친구. Google에게 물어봅시다.

 

헤이 구글, 쉐이더가 뭔지 알려줘!

Shader[셰이더]
컴퓨터 그래픽스 분야에서 셰이더(shader)는 소프트웨어 명령의 집합으로 주로 그래픽 하드웨어의 렌더링 효과를 계산하는 데 쓰인다. 셰이더는 그래픽 처리 장치(GPU)의 프로그래밍이 가능한 렌더링 파이프라인을 프로그래밍하는 데 쓰인다.

··· ··· ··· ···.

 

더 모르겠잖아!

 

음, 농담은 그만하고. 조금 더 간단하게 생각해봅시다.

Shader은 'Shade'와 '-er'의 합성어죠.

 

Shade: 명사 | 그늘, 색조, 음영

-er: (명사에서) | '무엇을 하는 사람이나 도구, 기계 등'을 나타냄

 

즉, 그늘·색조·음영 등의 효과를 주는 주체(도구, 기계 등의 무언가)

정도로 해석할 수 있겠네요.

 

 

#렌더링 파이프라인

다시 구글님의 설명으로 돌아와볼까요.

··· (중략) ···
셰이더는 그래픽 처리 장치(GPU)의 프로그래밍이 가능한 
렌더링 파이프라인을 프로그래밍하는 데 쓰인다.

생소한 단어가 나왔네요. '렌더링 파이프라인'.

다시 한 번 영어사전을 펴 볼까요?

 

rendering: 명사 | 아직 제품화되지 않고 계획 단계에 있는 공업 제품을누구나  외관을 이해할  있도록 실물 그대로 그린 완성 예상도주로 디자인 용어로 쓴다.

pipeline: 명사 | 석유나 천연가스 따위를 수송하기 위하여 매설한 관로().

 

아직 제품화 되지 않고 계획 단계에 있는 공업 제품 → 모니터에 출력되기 전, 3D 오브젝트로 존재하는 물체들

석유나 천연가스 따위 CPU, GPU에게 넘겨줄 명령들.

 

이 정도로 하면 이해가 되려나요?

3D 오브젝트가 우리가 보는 모니터에 띄워지는 과정.

가상의 3차원 공간(게임의 공간)2차원(모니터)으로 출력하기 위해
원근 개념을 도입하여
화면에 출력하는 일련의 과정.

정도로 렌더링 파이프라인을 정의내릴 수 있습니다.

그리고 이 과정에 개입할 수 있는 것이 쉐이더구요.

 

렌더링 파이프라인의 과정은 매우매우매우매우x100 복잡한데, 

아주 간략하게 정리해 보면 이렇게 됩니다.

 

간략화시킨 렌더링 파이프라인.  출처: https://kblog.popekim.com/2011/11/01-part-1.html

 

여기서, 버텍스(정점) 쉐이더'버텍스의 좌표 값'을 제어.

픽셀 쉐이더는 오브젝트의 '조명, 색, 텍스처'를 제어하게 됩니다.

사실 저도 자세하게는 몰라요! 3학년 때 자세하게 배운대요!

 

 

아무튼 여기서는 픽셀 쉐이더를 다룰 겁니다.

 

 

#'픽셀 쉐이더' 가지고 놀기! 빛나는 원 만들기.

드디어 실습시간이에요! 실습이 역시 재밌죠. 안 그래요?

이론을 알아야 실습도 잘 하지. 이 사람아...

 

일단 유니티를 켜 봅시다.

 

유니티 기본 화면.

먼저 빛을 내게 할 물체를 먼저 꺼내야 하겠죠.

Hierarchy창에서 마우스 우 클릭 - 3D object - Ouad

 

로 오브젝트를 만들어줍시다.

 

Hierarchy 창 → 3D object → Quad

※Quad 오브젝트는 3dsMax에서의 plane 오브젝트처럼 뒷면이 렌더링 되지 않으니 주의할 것!

 

이렇게 해서, 평면인 오브젝트를 하나 꺼냈습니다.

그런데... 우리는 빛나는 '원'을 만들 거잖아요? 이건 사각형인데.

 

없으면 만드는게 게임 개발자의 자질이죠.

동그란 텍스쳐를 만들어 줍시다.

 

크기 128*128, 해상도 72인치로 만들어준다!

포토샵에서 크기 128*128에 해상도 72의 캔버스를 하나 만들어 줍시다.

 

Q. 왜 128*128 사이즈 인가요?

A. 첫 번째 이유로는, 텍스쳐의 크기는 2의 n승을 지켜야 합니다.

(...라고 하더라구요. 검색해보니 이유가 꽤 많던데... 일단 너무 많고, 다 이해한 것도 아니라 패스. 압축할 때 자동으로 맞춰 주기도 한다는데, 일단 안전하게 2의 n승 사이즈로 만들어 줍시다!)

두 번째 이유로는, 이펙트는 그래픽 중에서 부하가 가장 심하기 때문입니다.

이펙트의 질과 프로그램의 최적화 사이에서 타협을 잘 해야 한다는 의미겠죠?

 

Q. 왜 해상도는 72인가요?

A. 우리는 '모니터'에서 이를 볼 것이기 때문입니다. 출력물이 아닐 경우 72 이상의 해상도는 큰 의미가 없습니다.

 

원을 하나 만들어 준다.

뭐, 그럼 이제 뻔할 뻔자죠.

검은색 배경을 깔아주고, 원을 하나 그려줍니다.

 

그리고 채널로 가서.

 

알파채널에서도 원 만들어 줌.

Alpha 채널을 만들어주고, RGB모양 선택 후 페인트.

TGA 파일로 저장해줍니다.

 

이 때, 32bit로 저장해야지 알파가 적용되니 꼭 확인!

 

이제 유니티로 돌아와, 방금 내보낸 TGA 이미지를 불러와 준다.

만든 텍스쳐파일을 유니티로 불러왔습니다.

이제 적용시킬 재질을 만들어 줘야 겠죠?

 

Material을 생성 해, Quad 오브젝트에 적용해주고,

이미지를 넣기 위한 쉐이더도 만들어 줍시다. 

 

(Asset창 - 마우스 우클릭 - Material

Asset창 - 마우스 우클릭 - Shader - New Surface Shader)

 

 

Quad 오브젝트에 Material을 적용한 후에,

우측의 Inspector 창에서 Shader를 [Custom > 방금 만든 쉐이더 이름]으로 설정해주고,

바로 아래의 None(texture)이라고 적혀있는 곳에 이미지를 넣어줍시다.

 

다 적용한 후의 화면.

어... 여전히 동그랗지 않은데요. 그냥 무늬만 생겼는걸요?

 

아직 설정할게 남아서 그렇습니다!

 

불러온 TGA 이미지 파일의 Inspector창을 보면,

TGA 파일의 Inspector 창.

우리는 여기서 딱 3개만 설정하면 됩니다.

 

Alpha Is Transparency: 투명도(알파)를 적용 시켜준다는 옵션. 체크 해 줍시다.

Generate Mip Map: Mip Map* 을 생성 한다는 옵션. 체크 해제 해 줍시다.

*MipMap: 렌더링 속도를 향상시키기 위한 목적으로 기본 텍스처와 이를 연속적으로 미리 축소시킨 텍스처들로 이루어진 비트맵 이미지의 집합. 시야에서 멀어질수록 낮은 텍스쳐로 이미지를 대체해서 보여주는데, 이펙트는 해상도가 중요합니다. 안 그래도 최대한 이미지를 작게 쓰고 있는데 말이에요!

Default의 Compression: 이미지의 압축률을 정하는 옵션. None으로 바꿔 줍시다.

 

이렇게 체크해주면, 짜잔!

... 더 이상해 진 것 같다구요? 정상입니다.

왜냐하면, 텍스처에서 투명도를 적용해줬지만, 쉐이더에서는 투명도를 받으라고 설정해주지 않았기 때문이에요.

 

그럼 이제, 쉐이더를 열어봅시다.

 

오... 익숙한 듯 익숙하지 않은데...

코딩을 해본 사람들은 그래도 익숙할 수도 있고, 아니라면 오... 이게 뭐여... 할 수도 있겠네요.

자, 하나하나씩 살펴봅시다.

Shader "Custom/NewSurfaceShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

유니티 쉐이더의 자체 스크립트 부분입니다.

 

이곳에 쉐이더의 이름과 트리구조를 정할 수 있고,

특정 구문을 적으면 Material의 Inspector에 인터페이스가 생깁니다.

 

Shader "Custom/NewSurfaceShader"

쉐이더의 이름입니다. /를 쓰면 트리구조가 생기며,

제일 마지막에 쓴 이름이 쉐이더의 이름이 됩니다.

 

※쉐이더명과 파일명은 별개이니, 주의할 것!

    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _Glossiness ("Smoothness", Range(0,1)) = 0.5
        _Metallic ("Metallic", Range(0,1)) = 0.0
    }

Inspector에 직접적으로 인터페이스를 만들어 줍니다.

 

예를 들어, 위 코드의 _Color ("Color", Color) = (1, 1, 1, 1) 부분은

Material의 Inspector에 컬러피커를 만들어 줍니다.

 

 

    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

렌더링 타입을 설정하는 부분입니다.

현재 설정되어 있는 것은 "Opaque". 즉 <불투명> 입니다.

 

위에서 투명도 설정을 해주었는데도 제대로 보이지 않았던 이유가 이것!


CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

CGPROGRAM 영역입니다.

유니티 서피스 쉐이더는 자체 스크립트와 CGPROGRAM 언어를 함께 사용하고 있습니다.

이 부분이 우리가 주 된 쉐이더 코딩을 하는 부분입니다.

 

        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf Standard fullforwardshadows

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0

Snippet[전처리] 부분입니다.

쉐이더의 조명계산이나, 세부적인 분기를 지정해줍니다.

 

        struct Input
        {
            float2 uv_MainTex;
        };

Input이라는 이름을 가진 구조체(Structure)입니다.

중괄호{} 안에는 엔진으로부터 받아야 할 데이터들이 들어갑니다.

 

코딩을 해보신 분들에게는 익숙할 수도 있겠지만, 중괄호 끝에 ;(세미콜론)이 붙어야 한다는 점을 주의하세요!

 

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            // Albedo comes from a texture tinted by color
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            // Metallic and smoothness come from slider variables
            o.Metallic = _Metallic;
            o.Smoothness = _Glossiness;
            o.Alpha = c.a;
        }
        ENDCG

surf라는 이름을 가진 함수 영역 입니다.

색상이나 이미지가 출력되게 만들 수 있는 부분입니다.

 

사용할 수 있는 프로퍼티 중 일부입니다.

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

 

역시나 코딩을 해보신 분들에게는 익숙할 수도 있겠지만,

여기서는 중괄호 끝에 ;(세미콜론)이 없다는 점을 주의하세요!

 

대충 설명은 끝났으니, 지금 우리에게 필요 없는 부분을 지워봅시다!

 

붉은색, 노란색 부분을 전부 지워주세요!

먼저 주석들을 전부 지우고, (붉은색)

거칠기나 금속 여부도 현재 사용하지 않으니 지워줍시다. (노란색)

 

붉은색 부분을 지워주세요!

빛 추가 연산을 하는 코드도 현재는 필요 없으니 제거해줍시다.

 

(fullforwardshadows: 그림자 및 공간 분할 처리 방법을 제어하기 위해 추가 지시문. 포워드 렌더링 경로 에서 모든 라이트 섀도우 유형을 지원합니다.)

 

Shader "Custom/NewSurfaceShader"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 200

        CGPROGRAM

        #pragma surface surf Standard
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
            float2 uv_MainTex;
        };

        fixed4 _Color;

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

다 하고 나면, 깔끔해졌네요!

자, 그럼 이제 투명도를 적용해볼까요?

 

아까의 이상한 현상이 나타난 이유는 렌더링 타입이 "Opaque". 즉, 불투명으로 되어 있어서 였습니다.

그렇다면...

    SubShader
    {
        Tags { "RenderType"="Transparent" }
        LOD 200 

이렇게 렌더링 타입을 "Transparent"(투명한) 으로 바꿔주면 되겠네요!

 

아, 추가로 그 뒤에 더 적어 볼게요!

    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
        LOD 200 

뒤의 "Queue" = "Transparent"는 렌더링 순서를 정해주는 코드입니다.

지금은 큰 상관 없으니 넘어가셔도 괜찮아요! 안 적어도 문제는 없습니다!

 

        CGPROGRAM

        #pragma surface surf Standard alpha:fade
        #pragma target 3.0

이 부분에도 alpha:fade를 추가해주세요!

여기까지 하신 후 저장하시고 유니티를 보면,

 

드디어 투명해졌다! 이제 빛만 내면 되겠네요!

투명도 적용이 끝났습니다!

이제 빛을 내는 방법만 알면 되겠네요.

 

다시 쉐이더로 돌아옵시다.

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Emission = float3(1, 1, 1);
            //o.Albedo = c.rgb;
            o.Alpha = c.a;
        }
        

o.Albedo를 주석처리하거나 지우고, o.Emission을 추가해줍시다.

 

두 속성은 둘 다 RGB값을 사용하기 위해 float3 이상을 사용합니다.

float3은 (R, G, B) float4는 (RGB, 투) 입니다.

 

그럼 뭐가 다르냐!

Albedo는 조명 연산을 받으나

Emission은 조명 연산을 받지 않습니다.

 

정확히는 Albedo는 기본 색상을 나타내지만, Emission은 이 물체가 빛을 발하는 정도를 나타냅니다.
Albedo를 지우고 Emission만 사용할 경우, 조명 연산의 적용을 받는 기본 색은 존재하지 않으나,
빛은 발하고 있는 상태가 됩니다.  (정말로 빛 그 자체가 되어버리는)

자, 그럼 여기서 우리는 색상을 어떻게 나타낼 수 있을까요?

 

빛의 3원색

네, 당연하게도 빛의 3원색을 사용합니다.

o.Emission의 float에서, 0~1 사이의 소수점 값은 빛의 세기를 나타냅니다.

(0%~100%를 0과 1, 그리고 사이의 소수로 표시한다고 보면 되겠네요.)

 

빨간색 빛이 50%, 초록색 빛이 50%, 파란색 빛이 50% 비춰진다면?

당연하게도 회색이 되겠죠!

 

그렇다면 현재 코드에서의 o.Emission의 색상은?

 

네, 당연하게도 흰색입니다.

R 100%, G 100%, B 100%니까요.

 

그렇다면, 여기서 1보다 더 큰 수치를 가지면 어떻게 될까요?

해보면 알겠죠.

        void surf (Input IN, inout SurfaceOutputStandard o)
        {
            fixed4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color;
            o.Emission = float3(2, 1, 1);
            //o.Albedo = c.rgb;
            o.Alpha = c.a;
        }

 

어떻게 될까요? R이 200%니, 빨간색이 나오려나요?

 

그냥 흰색이 나옵니다.

(묘-하게 테두리에 빨간 빛이 돌긴 하는데. 이는 텍스쳐를 만들 때 테두리 부분이 완벽한 흰색이 아니었기 때문.)

 

왜 그냥 흰색이 나올까요? 빨간색 값이 더 높으니, 빨간색이 나와야 하는 게 아닐까요?

이유는 모니터가 200%라는 색을 출력하지 못하기 때문입니다.

 

이런 현상을 바로 HDR이라고 부릅니다.

High Dynamic Range [ HDR ]

카메라 용어에서 나온 것으로,

카메라에서 과노출로 사진을 찍었을 때, 하얗게 날아간 부분이 그냥 하얀색이 아닌

그저 표현하지 못하는 굉장히 밝은 색이라는 것.

이를 컴퓨터 그래픽스에서는 1보다 높은 값으로 구현해두었으며,

이렇게 날아간 밝은 색을 표현하는 방법, 혹은 그 기술을 

High Dynamic Range[HDR] 이라고 부른다.

정확한 내용은 아닐 수 있습니다. 이해한 것을 그대로 썼을 뿐...!

 

그럼 우리는 이 과도한 밝음을 어떻게 표현할까요?

여러 방법이 존재하겠지만, 우리는 Bloom을 통해 표현하기로 합시다.

"Bloom이 뭔데요...?"

쉽게 말해서, 빛 번짐 효과 입니다.

이런거 있잖아요, 이런 거. 밝은 물체 주변이 빛으로 번지는 현상. 

후처리 기능인 Post-Processing을 이용하여 Bloom을 넣어보도록 합시다.

 

일단 Post-Processing을 유니티에서 설치 해 줍시다.

Window - Package Manager
포스트 프로세싱을 검색해 설치해준다.

 

설치 하셨나요?

이제 와다다다 하기만 하면 됩니다.

Hierachy 창 - 우클릭 - 3D Object - Post process Volume 생성.
Asset창 - 마우스 우클릭 - Post processing Profile 생성.
Post-process Volume을 선택, Instpector창의 Post Process Volume(Script)의 Profie에 Post processing profile 넣기. (드래그 앤 드랍)
이후, 만약 레이어에 Post Processing이 없다면 직접 추가해주자! 있다면 패스!
Main Camera의 Post Processing Layer(Script)의 Layer를 PostProcessing으로.
Main Camera의 레이어도 PostProcessing으로.
Post-Process Volume의 레이어도 PostProcessing으로 바꿔주고, 아래쪽 Script의 is global도 체크해준다.

* is Global?

체크하지 않을 시, Volume 오브젝트의 박스 영역 안쪽에서만 적용됨.

전체적으로 적용하고 싶다면 is Global을 체크하자!

 

이제 Add effect를 눌러 Bloom을 넣고,
체크를 해준 후에, Intensity 값을 올리면...?

 

짠. bloom 효과 완성이다.

o.Emission = float3(2, 1, 1); 이었으니 빛번짐에서 붉은 빛이 감돈다. (R이 200%이므로.)

o.Emission = float3(1, 2, 1); 를 한다면?

 

당연하게도 초록빛이 빤딱!

(만족)

 

응용하면 별모양으로 반짝이게도 바꿀 수 있겠다. 텍스쳐만 바꾸면 되니.

댓글